@astrosheep/keiyaku 2.9.11 → 2.9.13

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 (218) hide show
  1. package/README.md +79 -85
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms.js +1 -0
  4. package/build/agents/providers/claude-agent-sdk/adapter.js +17 -4
  5. package/build/cli/commands/akuma/akuma/handler.js +2 -1
  6. package/build/cli/commands/akuma/akuma/meta.js +4 -3
  7. package/build/cli/commands/akuma/catalog.js +2 -0
  8. package/build/cli/commands/akuma/list/handler.js +1 -1
  9. package/build/cli/commands/akuma/list/meta-list.js +12 -0
  10. package/build/cli/commands/akuma/list/meta-ls.js +2 -2
  11. package/build/cli/commands/akuma.js +5 -0
  12. package/build/cli/commands/contract/amend/meta.js +4 -3
  13. package/build/cli/commands/contract/arc/meta.js +2 -0
  14. package/build/cli/commands/contract/audit/handler.js +28 -2
  15. package/build/cli/commands/contract/audit/meta.js +4 -3
  16. package/build/cli/commands/contract/bind/meta.js +4 -4
  17. package/build/cli/commands/contract/petition/handler.js +2 -1
  18. package/build/cli/commands/contract/petition/meta.js +2 -2
  19. package/build/cli/commands/contract/renew/handler.js +44 -3
  20. package/build/cli/commands/contract/renew/meta.js +5 -4
  21. package/build/cli/commands/metadata.js +14 -10
  22. package/build/cli/commands/projection/call/handler.js +1 -1
  23. package/build/cli/commands/projection/call/meta.js +1 -1
  24. package/build/cli/commands/projection/catalog.js +2 -2
  25. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  26. package/build/cli/commands/projection/fork/meta.js +12 -0
  27. package/build/cli/commands/projection/history/handler.js +46 -2
  28. package/build/cli/commands/projection/history/meta.js +3 -3
  29. package/build/cli/commands/projection/kill/handler.js +4 -5
  30. package/build/cli/commands/projection/kill/meta.js +2 -2
  31. package/build/cli/commands/projection/status/handler.js +29 -13
  32. package/build/cli/commands/projection/status/meta.js +14 -3
  33. package/build/cli/commands/projection/tell/handler.js +8 -4
  34. package/build/cli/commands/projection/tell/meta.js +1 -1
  35. package/build/cli/commands/projection/wait/handler.js +45 -20
  36. package/build/cli/commands/projection/wait/meta.js +1 -1
  37. package/build/cli/commands/shared.js +2 -2
  38. package/build/cli/commands/system/catalog.js +2 -0
  39. package/build/cli/commands/task/add/handler.js +28 -12
  40. package/build/cli/commands/task/add/meta.js +18 -6
  41. package/build/cli/commands/task/catalog.js +4 -0
  42. package/build/cli/commands/task/compose/handler.js +18 -0
  43. package/build/cli/commands/task/compose/meta.js +10 -0
  44. package/build/cli/commands/task/doctor/handler.js +2 -3
  45. package/build/cli/commands/task/doctor/meta.js +2 -2
  46. package/build/cli/commands/task/done/meta.js +2 -2
  47. package/build/cli/commands/task/drop/handler.js +7 -2
  48. package/build/cli/commands/task/drop/meta.js +6 -3
  49. package/build/cli/commands/task/hold/meta.js +2 -2
  50. package/build/cli/commands/task/log/meta.js +2 -2
  51. package/build/cli/commands/task/ls/meta.js +2 -2
  52. package/build/cli/commands/task/note/handler.js +6 -0
  53. package/build/cli/commands/task/note/meta.js +11 -0
  54. package/build/cli/commands/task/resume/meta.js +2 -2
  55. package/build/cli/commands/task/shared.js +29 -15
  56. package/build/cli/commands/task/show/meta.js +2 -2
  57. package/build/cli/commands/task/start/meta.js +2 -2
  58. package/build/cli/commands/task/stop/meta.js +2 -2
  59. package/build/cli/commands/task/update/meta.js +3 -2
  60. package/build/cli/commands/verification/handler.js +43 -0
  61. package/build/cli/commands/verification/meta.js +13 -0
  62. package/build/cli/completion.js +16 -13
  63. package/build/cli/flags.js +48 -12
  64. package/build/cli/help.js +29 -30
  65. package/build/cli/index.js +14 -8
  66. package/build/cli/parse-flags.js +60 -21
  67. package/build/cli/parse-metadata.js +10 -4
  68. package/build/cli/parse-selectors.js +22 -4
  69. package/build/cli/parse.js +43 -30
  70. package/build/cli/projection-address.js +7 -7
  71. package/build/cli/render/arc.js +12 -3
  72. package/build/cli/render/audit.js +2 -2
  73. package/build/cli/render/call.js +4 -4
  74. package/build/cli/render/kanshi.js +122 -85
  75. package/build/cli/render/misc.js +2 -2
  76. package/build/cli/render/petition.js +16 -1
  77. package/build/cli/render/projection-history.js +17 -4
  78. package/build/cli/render/shared.js +5 -5
  79. package/build/cli/render/status-indicator.js +47 -0
  80. package/build/cli/render/status.js +106 -76
  81. package/build/cli/render/success-response.js +20 -5
  82. package/build/cli/render/task.js +33 -5
  83. package/build/cli/render/tell.js +10 -4
  84. package/build/cli/render/tool-command-normalization.js +137 -0
  85. package/build/cli/render/tool-presentation.js +2 -1
  86. package/build/cli/render/verification.js +23 -0
  87. package/build/cli/render/wait.js +101 -33
  88. package/build/cli/types.js +2 -1
  89. package/build/config/akuma-loader.js +21 -1
  90. package/build/config/settings/knobs.js +7 -1
  91. package/build/config/settings/schema.js +10 -5
  92. package/build/core/addressing.js +3 -3
  93. package/build/core/amend.js +102 -17
  94. package/build/core/arc.js +4 -4
  95. package/build/core/audit/candidate.js +10 -18
  96. package/build/core/audit/coordinates.js +12 -17
  97. package/build/core/audit/evidence.js +6 -4
  98. package/build/core/audit/facade.js +3 -3
  99. package/build/core/audit/report.js +1 -1
  100. package/build/core/bind-reconciliation.js +42 -23
  101. package/build/core/bind.js +182 -81
  102. package/build/core/call/call.js +6 -7
  103. package/build/core/call/context.js +46 -33
  104. package/build/core/call/execution.js +21 -6
  105. package/build/core/call/prompt.js +11 -42
  106. package/build/core/contract-carrier-runtime.js +209 -105
  107. package/build/core/contract-carrier.js +156 -73
  108. package/build/core/contract-view.js +43 -6
  109. package/build/core/contract.js +2 -1
  110. package/build/core/derived-replay.js +168 -35
  111. package/build/core/draft.js +60 -27
  112. package/build/core/forfeit.js +1 -1
  113. package/build/core/lifecycle-history.js +13 -0
  114. package/build/core/lifecycle-recovery.js +32 -27
  115. package/build/core/lifecycle-runner.js +14 -14
  116. package/build/core/log.js +1 -1
  117. package/build/core/outcome-base.js +2 -2
  118. package/build/core/projection/akuma-name.js +43 -0
  119. package/build/core/projection/generation/database.js +141 -79
  120. package/build/core/projection/generation/ledger.js +91 -0
  121. package/build/core/projection/generation/model.js +19 -14
  122. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  123. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  124. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  125. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  126. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  127. package/build/core/projection/generation/projection-generation-process.js +16 -15
  128. package/build/core/projection/generation/projection-generation-runner.js +132 -76
  129. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  130. package/build/core/projection/generation/protocol.js +68 -0
  131. package/build/core/projection/generation/store.js +17 -7
  132. package/build/core/projection/generation/transitions.js +24 -14
  133. package/build/core/projection/index.js +5 -5
  134. package/build/core/projection/projection-core.js +6 -2
  135. package/build/core/projection/projection-execution-observer.js +1 -1
  136. package/build/core/projection/projection-history.js +8 -8
  137. package/build/core/projection/projection-kill.js +20 -10
  138. package/build/core/projection/projection-life-observer.js +8 -8
  139. package/build/core/projection/projection-mint.js +6 -8
  140. package/build/core/projection/projection-status-observation.js +77 -8
  141. package/build/core/projection/projection-status.js +151 -4
  142. package/build/core/projection/projection-wait.js +48 -29
  143. package/build/core/projection/projection-wake.js +28 -88
  144. package/build/core/projection/tell/launch-store.js +1 -1
  145. package/build/core/projection/tell/store.js +1 -1
  146. package/build/core/renew-build.js +204 -74
  147. package/build/core/renew-plan.js +34 -9
  148. package/build/core/renew-rewrite.js +56 -15
  149. package/build/core/renew.js +39 -35
  150. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  151. package/build/core/repository-ledger/claim-evidence.js +1 -1
  152. package/build/core/repository-ledger/codec.js +11 -5
  153. package/build/core/repository-ledger/current-state-store.js +464 -349
  154. package/build/core/repository-ledger/fold-repository.js +5 -6
  155. package/build/core/repository-ledger/identity.js +60 -0
  156. package/build/core/repository-ledger/inventory.js +3 -3
  157. package/build/core/repository-ledger/read-model.js +57 -9
  158. package/build/core/repository-ledger/write-transaction.js +24 -17
  159. package/build/core/run-control/connection-bound-close.js +41 -0
  160. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  161. package/build/core/run-control/detached-process.js +14 -0
  162. package/build/core/run-control/index.js +6 -0
  163. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  164. package/build/core/run-control/runner-lease.js +172 -0
  165. package/build/core/run-control/sqlite-process-lifetime-lock.js +174 -0
  166. package/build/core/seal.js +44 -32
  167. package/build/core/settlement/claim-delivery.js +22 -9
  168. package/build/core/settlement/claim.js +8 -6
  169. package/build/core/settlement/petition-claim-gates.js +9 -8
  170. package/build/core/settlement/petition-head-guard.js +5 -6
  171. package/build/core/settlement/petition-preview.js +21 -5
  172. package/build/core/settlement/petition.js +51 -17
  173. package/build/core/settlement/settlement.js +20 -15
  174. package/build/core/settlement/verdict.js +2 -2
  175. package/build/core/settlement/verification.js +492 -364
  176. package/build/core/status/board.js +184 -75
  177. package/build/core/status/drift.js +8 -10
  178. package/build/core/task/board.js +1 -0
  179. package/build/core/task/commands.js +25 -0
  180. package/build/core/task/compose.js +328 -0
  181. package/build/core/task/document.js +102 -39
  182. package/build/core/task/index.js +4 -3
  183. package/build/core/task/settlement-git.js +70 -30
  184. package/build/core/task/settlement-policy.js +27 -14
  185. package/build/core/task/source-board.js +1 -0
  186. package/build/core/task/task-bind-preparation.js +59 -9
  187. package/build/core/task/task-contract.js +2 -2
  188. package/build/core/task/task-git-store.js +1 -0
  189. package/build/core/task/task-store-repository.js +2 -0
  190. package/build/core/task/task.js +2 -2
  191. package/build/core/transcripts.js +4 -4
  192. package/build/core/verification-declaration.js +95 -0
  193. package/build/core/worktree-bootstrap.js +1 -1
  194. package/build/core/worktree-path.js +7 -11
  195. package/build/flow-error.js +15 -2
  196. package/build/generated/version.js +2 -2
  197. package/build/git/branches.js +41 -70
  198. package/build/git/commits.js +92 -21
  199. package/build/git/core.js +9 -161
  200. package/build/git/diff/preview.js +4 -4
  201. package/build/git/diff/read.js +4 -4
  202. package/build/git/diff/structured.js +5 -5
  203. package/build/git/process.js +229 -0
  204. package/build/git/refs.js +30 -5
  205. package/build/git/staging.js +39 -115
  206. package/build/git/streaming-batch.js +9 -16
  207. package/build/git/worktree.js +104 -91
  208. package/build/index.js +7 -0
  209. package/package.json +2 -2
  210. package/skills/keiyaku-akuma/SKILL.md +27 -21
  211. package/skills/keiyaku-task/SKILL.md +61 -5
  212. package/skills/keiyaku-workflow/SKILL.md +81 -26
  213. package/build/cli/commands/projection/revive/meta.js +0 -12
  214. package/build/core/projection/projection-life-protocol.js +0 -115
  215. package/build/core/projection/projection-runner-lock.js +0 -382
  216. package/build/core/projection/tell/database.js +0 -127
  217. package/build/core/settlement/verification-coordination.js +0 -305
  218. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -1,6 +1,13 @@
1
- import { GENERATION_VERDICT_STATES, } from "../projection-life-protocol.js";
2
1
  import { OutcomeErrorValidationError, parseCurrentOutcomeError, } from "../../../agents/harness/index.js";
3
- export { GENERATION_VERDICT_STATES };
2
+ export const GENERATION_VERDICT_STATES = [
3
+ "completed",
4
+ "failed",
5
+ "launch-failed",
6
+ "lost-runner",
7
+ "dismissed",
8
+ "interrupted",
9
+ "killed",
10
+ ];
4
11
  export const VERDICTS = new Set(GENERATION_VERDICT_STATES);
5
12
  export class ProjectionGenerationStoreError extends Error {
6
13
  code = "PROJECTION_GENERATION_STORE_ERROR";
@@ -12,17 +19,17 @@ export class ProjectionGenerationStoreError extends Error {
12
19
  this.observation = options?.observation ?? "unknown";
13
20
  }
14
21
  }
15
- export function isIncompatibleProjectionGenerationStoreError(error) {
16
- return error instanceof ProjectionGenerationStoreError
17
- && error.observation === "incompatible";
18
- }
19
- export function isPlainObject(value) {
22
+ function isPlainObject(value) {
20
23
  if (value === null || typeof value !== "object" || Array.isArray(value))
21
24
  return false;
22
25
  const prototype = Object.getPrototypeOf(value);
23
26
  return prototype === Object.prototype || prototype === null;
24
27
  }
25
- export function assertJsonValue(value, location, ancestors) {
28
+ export function isIncompatibleProjectionGenerationStoreError(error) {
29
+ return error instanceof ProjectionGenerationStoreError
30
+ && error.observation === "incompatible";
31
+ }
32
+ export function assertJsonValue(value, location, ancestors = new Set()) {
26
33
  if (value === null || typeof value === "string" || typeof value === "boolean")
27
34
  return;
28
35
  if (typeof value === "number") {
@@ -30,9 +37,8 @@ export function assertJsonValue(value, location, ancestors) {
30
37
  throw new ProjectionGenerationStoreError(`${location} contains a non-finite number`);
31
38
  return;
32
39
  }
33
- if (typeof value !== "object") {
40
+ if (typeof value !== "object")
34
41
  throw new ProjectionGenerationStoreError(`${location} contains a non-JSON value`);
35
- }
36
42
  if (ancestors.has(value))
37
43
  throw new ProjectionGenerationStoreError(`${location} contains a cycle`);
38
44
  ancestors.add(value);
@@ -43,9 +49,8 @@ export function assertJsonValue(value, location, ancestors) {
43
49
  }
44
50
  if (!isPlainObject(value))
45
51
  throw new ProjectionGenerationStoreError(`${location} must contain only plain JSON objects`);
46
- for (const [key, item] of Object.entries(value)) {
52
+ for (const [key, item] of Object.entries(value))
47
53
  assertJsonValue(item, `${location}.${key}`, ancestors);
48
- }
49
54
  }
50
55
  finally {
51
56
  ancestors.delete(value);
@@ -54,7 +59,7 @@ export function assertJsonValue(value, location, ancestors) {
54
59
  export function cloneFacts(value, location) {
55
60
  if (!isPlainObject(value))
56
61
  throw new ProjectionGenerationStoreError(`${location} must be a JSON object`);
57
- assertJsonValue(value, location, new Set());
62
+ assertJsonValue(value, location);
58
63
  return JSON.parse(JSON.stringify(value));
59
64
  }
60
65
  export function assertExecutionId(executionId) {
@@ -247,7 +252,7 @@ export function validateHistory(records) {
247
252
  export function extractTerminalTimestamp(facts) {
248
253
  if (!facts)
249
254
  return undefined;
250
- for (const key of ["completedAt", "failedAt", "dismissedAt", "killedAt", "terminalAt"]) {
255
+ for (const key of ["terminalAt", "completedAt", "failedAt", "dismissedAt", "killedAt"]) {
251
256
  const value = facts[key];
252
257
  if (typeof value === "string" && Number.isFinite(Date.parse(value)))
253
258
  return value;
@@ -52,7 +52,7 @@ function readContinuationFromDatabase(database, limit = 10) {
52
52
  /** One heart snapshot owns generation selection and all continuation facts. */
53
53
  export function readProjectionGenerationContinuationSnapshot(projectionDirectory, limit = 10) {
54
54
  return withGenerationReadSnapshot(projectionDirectory, (database) => {
55
- const { records, current } = readValidatedHistory(database);
55
+ const { records, current } = readValidatedHistory(database, true);
56
56
  const continuation = readContinuationFromDatabase(database, limit);
57
57
  return {
58
58
  records,
@@ -0,0 +1,88 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as net from "node:net";
4
+ const CONNECT_TIMEOUT_MS = 50;
5
+ const POSIX_PREFIX = "/tmp/keiyaku-doorbell-";
6
+ const WINDOWS_PREFIX = "\\\\.\\pipe\\keiyaku-doorbell-";
7
+ function endpointDigest(identity, executionId) {
8
+ return createHash("sha256")
9
+ .update(JSON.stringify(identity))
10
+ .update("\0")
11
+ .update(executionId)
12
+ .digest("hex");
13
+ }
14
+ /** A bounded, deterministic endpoint with no projection-directory pathname. */
15
+ export function projectionGenerationDoorbellAddress(identity, executionId) {
16
+ const digest = endpointDigest(identity, executionId);
17
+ return process.platform === "win32"
18
+ ? `${WINDOWS_PREFIX}${digest}`
19
+ : `${POSIX_PREFIX}${digest}.sock`;
20
+ }
21
+ function removeStalePosixSocket(address) {
22
+ try {
23
+ const stat = fs.lstatSync(address);
24
+ if (!stat.isSocket())
25
+ throw new Error(`doorbell endpoint is not a socket: ${address}`);
26
+ fs.unlinkSync(address);
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return;
31
+ throw error;
32
+ }
33
+ }
34
+ function listen(server, address) {
35
+ return new Promise((resolve, reject) => {
36
+ const onError = (error) => {
37
+ server.off("listening", onListening);
38
+ reject(error);
39
+ };
40
+ const onListening = () => {
41
+ server.off("error", onError);
42
+ resolve();
43
+ };
44
+ server.once("error", onError);
45
+ server.once("listening", onListening);
46
+ server.listen(address);
47
+ });
48
+ }
49
+ export async function openProjectionGenerationDoorbell(input) {
50
+ const address = projectionGenerationDoorbellAddress(input.identity, input.executionId);
51
+ if (process.platform !== "win32")
52
+ removeStalePosixSocket(address);
53
+ const server = net.createServer((socket) => {
54
+ socket.destroy();
55
+ input.onRing();
56
+ });
57
+ await listen(server, address);
58
+ return {
59
+ address,
60
+ close: async () => {
61
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
62
+ if (process.platform !== "win32") {
63
+ try {
64
+ fs.unlinkSync(address);
65
+ }
66
+ catch (error) {
67
+ if (error.code !== "ENOENT")
68
+ throw error;
69
+ }
70
+ }
71
+ },
72
+ };
73
+ }
74
+ /**
75
+ * Best-effort wake only. Connection result is deliberately discarded: heart
76
+ * durability and leash evidence remain the only product/liveness authority.
77
+ */
78
+ export function ringProjectionGenerationDoorbell(identity, executionId) {
79
+ const socket = net.createConnection(projectionGenerationDoorbellAddress(identity, executionId));
80
+ const timeout = setTimeout(() => socket.destroy(), CONNECT_TIMEOUT_MS);
81
+ timeout.unref?.();
82
+ const finish = () => {
83
+ clearTimeout(timeout);
84
+ socket.destroy();
85
+ };
86
+ socket.once("connect", finish);
87
+ socket.once("error", finish);
88
+ }
@@ -20,6 +20,27 @@ function requireString(value, field) {
20
20
  function optionalString(value, field) {
21
21
  return value === undefined ? undefined : requireString(value, field);
22
22
  }
23
+ function optionalCarrier(value) {
24
+ if (value === undefined)
25
+ return undefined;
26
+ if (!isObject(value) || value.path !== ".keiyaku/KEIYAKU.md" || !isObject(value.identity)) {
27
+ throw new Error("generation launch carrier must be a verified carrier coordinate");
28
+ }
29
+ const { identity } = value;
30
+ if (typeof identity.contractId !== "string" || identity.contractId.trim() === ""
31
+ || typeof identity.contractHead !== "string" || identity.contractHead.trim() === ""
32
+ || typeof identity.bodyDigest !== "string" || identity.bodyDigest.trim() === "") {
33
+ throw new Error("generation launch carrier identity must contain non-blank contractId, contractHead, and bodyDigest");
34
+ }
35
+ return {
36
+ path: ".keiyaku/KEIYAKU.md",
37
+ identity: {
38
+ contractId: identity.contractId,
39
+ contractHead: identity.contractHead,
40
+ bodyDigest: identity.bodyDigest,
41
+ },
42
+ };
43
+ }
23
44
  function requirePositiveSafeInteger(value, field) {
24
45
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
25
46
  throw new Error(`generation launch ${field} must be a positive safe integer`);
@@ -54,6 +75,7 @@ function parseLaunchInputs(launch) {
54
75
  const context = optionalString(raw.context, "context");
55
76
  const resumePath = optionalString(raw.resumePath, "resumePath");
56
77
  const sourceArtifactId = optionalString(raw.sourceArtifactId, "sourceArtifactId");
78
+ const carrier = optionalCarrier(raw.carrier);
57
79
  return {
58
80
  prompt: requireString(raw.prompt, "prompt"),
59
81
  cwd: requireString(raw.cwd, "cwd"),
@@ -69,6 +91,7 @@ function parseLaunchInputs(launch) {
69
91
  ...(raw.session ? { session: raw.session } : {}),
70
92
  ...(resumePath ? { resumePath } : {}),
71
93
  ...(sourceArtifactId ? { sourceArtifactId } : {}),
94
+ ...(carrier ? { carrier } : {}),
72
95
  };
73
96
  }
74
97
  function persistenceError(error) {
@@ -161,6 +184,7 @@ export function prepareProjectionGenerationExecution(projectionDirectory, launch
161
184
  idleTimeoutMs: inputs.idleTimeoutMs,
162
185
  executionTimeoutMs: inputs.executionTimeoutMs,
163
186
  initialTurn: maximumValidEnvelopeTurn(projectionDirectory),
187
+ carrier: inputs.carrier,
164
188
  }), providerLoaderPorts);
165
189
  const observerController = createProjectionExecutionObserver({
166
190
  projectionDirectory,
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { ProjectionStateError } from "../../atomic-publish.js";
4
4
  import { createUlid } from "../../ids.js";
5
5
  const EXECUTION_ID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/;
6
+ export const PROJECTION_RUNNER_LEASE_FILE = "leash";
6
7
  export function createProjectionExecutionId(nowMs = Date.now()) {
7
8
  return createUlid(nowMs, [...randomBytes(10)]);
8
9
  }
@@ -15,3 +16,6 @@ export function executionStdioLogPath(projectionDirectory, executionId) {
15
16
  assertProjectionExecutionId(executionId);
16
17
  return path.join(projectionDirectory, `${executionId}.stdio.log`);
17
18
  }
19
+ export function projectionRunnerLeasePath(projectionDirectory) {
20
+ return path.join(projectionDirectory, PROJECTION_RUNNER_LEASE_FILE);
21
+ }
@@ -1,9 +1,11 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
+ import { PROJECTION_ADOPTION_TIMEOUT_DEFAULT_MS } from "../../../config/settings/knobs.js";
2
3
  import { openProjectionGenerationStore } from "./store.js";
3
- import { spawnProjectionGenerationRunner, } from "./projection-generation-process.js";
4
- import { acquireProjectionRunnerLock, observeProjectionRunnerLock, } from "../projection-runner-lock.js";
5
- export const PROJECTION_ADOPTION_TIMEOUT_MS = 4_000;
6
- export const PROJECTION_ADOPTION_POLL_MS = 20;
4
+ import { prepareProjectionGenerationRunner, } from "./projection-generation-process.js";
5
+ import { acquireRunnerLease, observeRunnerLease, } from "../../run-control/runner-lease.js";
6
+ import { openDetachedLeaseBootstrap } from "../../run-control/detached-lease-bootstrap.js";
7
+ import { projectionRunnerLeasePath } from "./projection-generation-identity.js";
8
+ const PROJECTION_ADOPTION_POLL_MS = 20;
7
9
  function errorDetail(error) {
8
10
  return error instanceof Error ? error.message : String(error);
9
11
  }
@@ -21,86 +23,39 @@ function recordDeadStartup(projectionDirectory, executionId, detail, stage, term
21
23
  store.close();
22
24
  }
23
25
  }
24
- function observeBootstrapMessages(child, executionId) {
25
- const seen = new Set();
26
- const waiters = new Map();
27
- let failure;
28
- const onMessage = (value) => {
29
- const message = value;
30
- if (message.executionId !== executionId)
31
- return;
32
- if (message.kind !== "leash-inherited" && message.kind !== "leash-acquired")
33
- return;
34
- seen.add(message.kind);
35
- const waiter = waiters.get(message.kind);
36
- if (waiter) {
37
- waiter.resolve();
38
- waiters.delete(message.kind);
39
- }
40
- };
41
- const onError = (error) => {
42
- failure = error;
43
- for (const waiter of waiters.values())
44
- waiter.reject(error);
45
- waiters.clear();
46
- };
47
- const onExit = () => onError(new Error("runner exited during bootstrap"));
48
- child.on("message", onMessage);
49
- child.on("error", onError);
50
- child.on("exit", onExit);
51
- return {
52
- waitFor(kind) {
53
- if (seen.has(kind))
54
- return Promise.resolve();
55
- if (failure)
56
- return Promise.reject(failure);
57
- return new Promise((resolve, reject) => {
58
- const timer = setTimeout(() => {
59
- waiters.delete(kind);
60
- reject(failure ?? new Error(`runner did not report ${kind} before startup timeout`));
61
- }, PROJECTION_ADOPTION_TIMEOUT_MS);
62
- waiters.set(kind, {
63
- resolve: () => {
64
- clearTimeout(timer);
65
- waiters.delete(kind);
66
- resolve();
67
- },
68
- reject: (error) => {
69
- clearTimeout(timer);
70
- waiters.delete(kind);
71
- reject(error);
72
- },
73
- });
74
- });
75
- },
76
- close() {
77
- child.off("message", onMessage);
78
- child.off("error", onError);
79
- child.off("exit", onExit);
80
- },
81
- };
82
- }
83
- function sendBootstrapCommand(child, command) {
84
- return new Promise((resolve, reject) => {
85
- if (!child.send || !child.connected) {
86
- reject(new Error("runner bootstrap channel is unavailable"));
87
- return;
88
- }
89
- child.send(command, (error) => error ? reject(error) : resolve());
90
- });
26
+ const ADOPTION_TIMEOUT_DETAIL = "runner did not adopt before projection adoption timeout";
27
+ async function failAdoptionDeadline(projectionDirectory, executionId, child, now, terminateChild, detail = ADOPTION_TIMEOUT_DETAIL) {
28
+ try {
29
+ await terminateChild(child);
30
+ }
31
+ catch (error) {
32
+ return { status: "failed", cause: "adoption", detail: `${detail}; runner termination could not be verified: ${errorDetail(error)}` };
33
+ }
34
+ const lease = observeRunnerLease(projectionRunnerLeasePath(projectionDirectory));
35
+ if (lease.state !== "released")
36
+ return {
37
+ status: "failed",
38
+ cause: "adoption",
39
+ detail: `${detail}; runner lock ${lease.state === "held" ? "remained held" : `could not be verified: ${lease.detail}`}`,
40
+ };
41
+ recordDeadStartup(projectionDirectory, executionId, detail, "adoption", new Date(now()).toISOString());
42
+ return { status: "failed", cause: "adoption", detail };
91
43
  }
92
44
  /** Spawn one committed launch and return only after its exact adoption row. */
93
45
  export async function startProjectionGeneration(projectionDirectory, executionId, dependencies = {}) {
94
46
  const now = dependencies.now ?? Date.now;
95
47
  const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
96
- const launcherAcquiredOwnership = dependencies.launchOwnership === undefined;
48
+ const adoptionTimeoutMs = dependencies.adoptionTimeoutMs ?? PROJECTION_ADOPTION_TIMEOUT_DEFAULT_MS;
49
+ // Bootstrap IPC and durable adoption are one launch operation, not retries
50
+ // with independent startup allowances.
51
+ const deadline = now() + adoptionTimeoutMs;
97
52
  let launchOwnership;
98
53
  try {
99
54
  launchOwnership = dependencies.launchOwnership
100
- ?? acquireProjectionRunnerLock(projectionDirectory);
55
+ ?? acquireRunnerLease(projectionRunnerLeasePath(projectionDirectory));
101
56
  }
102
57
  catch (error) {
103
- if (observeProjectionRunnerLock(projectionDirectory).state === "held") {
58
+ if (observeRunnerLease(projectionRunnerLeasePath(projectionDirectory)).state === "held") {
104
59
  return { status: "pending" };
105
60
  }
106
61
  return {
@@ -109,11 +64,9 @@ export async function startProjectionGeneration(projectionDirectory, executionId
109
64
  detail: `runner construction failed: ${errorDetail(error)}`,
110
65
  };
111
66
  }
112
- let launchCommitted = false;
113
- let childPromoted = false;
114
- let child;
67
+ let prepared;
115
68
  try {
116
- child = spawnProjectionGenerationRunner(projectionDirectory, executionId, dependencies);
69
+ prepared = prepareProjectionGenerationRunner(projectionDirectory, executionId, dependencies);
117
70
  }
118
71
  catch (error) {
119
72
  const detail = `runner spawn failed: ${errorDetail(error)}`;
@@ -121,60 +74,48 @@ export async function startProjectionGeneration(projectionDirectory, executionId
121
74
  launchOwnership.close();
122
75
  return { status: "failed", cause: "spawn", detail };
123
76
  }
124
- const bootstrap = observeBootstrapMessages(child, executionId);
77
+ const opened = await openDetachedLeaseBootstrap({
78
+ coordinate: executionId,
79
+ leasePath: projectionRunnerLeasePath(projectionDirectory),
80
+ parentLease: launchOwnership,
81
+ launch: prepared.launch,
82
+ deadline,
83
+ now,
84
+ spawn: dependencies.spawn,
85
+ terminate: dependencies.terminateChild,
86
+ });
87
+ prepared.close();
88
+ if (opened.status === "failed") {
89
+ const detail = opened.stage === "spawn" ? `runner spawn failed: ${opened.detail}` : `runner construction failed: ${opened.detail}`;
90
+ recordDeadStartup(projectionDirectory, executionId, detail, opened.stage === "spawn" ? "spawn" : "adoption", new Date(now()).toISOString());
91
+ return { status: "failed", cause: opened.stage === "spawn" ? "spawn" : "adoption", detail };
92
+ }
93
+ let committed = true;
94
+ let commitFailure;
125
95
  try {
126
- await bootstrap.waitFor("leash-inherited");
127
- if (dependencies.commitLaunch && !dependencies.commitLaunch()) {
128
- await sendBootstrapCommand(child, { kind: "launch-rejected", executionId });
129
- if (launcherAcquiredOwnership)
130
- launchOwnership.close();
131
- return { status: "failed", cause: "adoption", detail: "generation launch transaction was rejected" };
132
- }
133
- launchCommitted = true;
134
- launchOwnership.releaseReservationForTransfer();
135
- await sendBootstrapCommand(child, { kind: "promote-leash", executionId });
136
- await bootstrap.waitFor("leash-acquired");
137
- childPromoted = true;
138
- await sendBootstrapCommand(child, { kind: "launch-committed", executionId });
139
- try {
140
- launchOwnership.close();
141
- }
142
- catch {
143
- // The promoted child owns lifecycle truth; parent guard cleanup is diagnostic only.
144
- }
96
+ committed = dependencies.commitLaunch?.() ?? true;
145
97
  }
146
98
  catch (error) {
147
- void sendBootstrapCommand(child, { kind: "launch-rejected", executionId }).catch(() => { });
148
- if (childPromoted) {
149
- try {
150
- launchOwnership.close();
151
- }
152
- catch { }
153
- return { status: "pending", child };
154
- }
155
- if (!launchCommitted) {
156
- launchOwnership.close();
157
- return { status: "failed", cause: "adoption", detail: `runner construction failed: ${errorDetail(error)}` };
158
- }
159
- try {
160
- launchOwnership.reclaimReservation();
161
- }
162
- catch {
163
- launchOwnership.close();
164
- return { status: "pending", child };
165
- }
166
- try {
167
- recordDeadStartup(projectionDirectory, executionId, `runner construction failed: ${errorDetail(error)}`, "adoption", new Date(now()).toISOString());
168
- }
169
- finally {
170
- launchOwnership.close();
171
- }
172
- return { status: "failed", cause: "adoption", detail: `runner construction failed: ${errorDetail(error)}` };
99
+ committed = false;
100
+ commitFailure = error;
173
101
  }
174
- finally {
175
- bootstrap.close();
102
+ if (!committed) {
103
+ const rejected = await opened.bootstrap.reject();
104
+ return {
105
+ status: "failed",
106
+ cause: "adoption",
107
+ detail: rejected.status === "failed"
108
+ ? rejected.detail
109
+ : commitFailure ? `generation launch transaction failed: ${errorDetail(commitFailure)}` : "generation launch transaction was rejected",
110
+ };
176
111
  }
177
- const deadline = now() + PROJECTION_ADOPTION_TIMEOUT_MS;
112
+ const authorized = await opened.bootstrap.authorize();
113
+ if (authorized.status === "failed") {
114
+ const detail = now() >= deadline ? ADOPTION_TIMEOUT_DETAIL : `runner construction failed: ${authorized.detail}`;
115
+ recordDeadStartup(projectionDirectory, executionId, detail, "adoption", new Date(now()).toISOString());
116
+ return { status: "failed", cause: "adoption", detail };
117
+ }
118
+ const child = authorized.child;
178
119
  for (;;) {
179
120
  const observedAt = now();
180
121
  const store = openProjectionGenerationStore(projectionDirectory);
@@ -202,19 +143,12 @@ export async function startProjectionGeneration(projectionDirectory, executionId
202
143
  finally {
203
144
  store.close();
204
145
  }
146
+ if (observeRunnerLease(projectionRunnerLeasePath(projectionDirectory)).state === "held") {
147
+ return { status: "adopted", child };
148
+ }
205
149
  if (observedAt >= deadline)
206
150
  break;
207
151
  await sleep(Math.max(0, Math.min(PROJECTION_ADOPTION_POLL_MS, deadline - observedAt)));
208
152
  }
209
- const store = openProjectionGenerationStore(projectionDirectory);
210
- try {
211
- const current = store.readCurrentGeneration();
212
- if (current?.launch.executionId === executionId && current.adoption) {
213
- return { status: "adopted", child };
214
- }
215
- }
216
- finally {
217
- store.close();
218
- }
219
- return { status: "pending", child };
153
+ return failAdoptionDeadline(projectionDirectory, executionId, child, now, dependencies.terminateChild ?? (await import("../../run-control/process-tree.js")).terminateProcessGroup);
220
154
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { spawn as nodeSpawn } from "node:child_process";
4
3
  import { fileURLToPath } from "node:url";
4
+ import { RUN_CONTROL_BOOTSTRAP_ENV } from "../../run-control/index.js";
5
5
  import { executionStdioLogPath } from "./projection-generation-identity.js";
6
6
  const MODULE_FILE = fileURLToPath(import.meta.url);
7
7
  // Lives at src/core/projection/generation/ — four levels up is the package root.
@@ -17,9 +17,7 @@ export function projectionGenerationRunnerArgv(projectionDirectory, executionId)
17
17
  executionId,
18
18
  ];
19
19
  }
20
- export const PROJECTION_RUNNER_BOOTSTRAP_ENV = "KEIYAKU_PROJECTION_RUNNER_BOOTSTRAP";
21
- export function spawnProjectionGenerationRunner(projectionDirectory, executionId, dependencies = {}) {
22
- const spawn = dependencies.spawn ?? nodeSpawn;
20
+ export function prepareProjectionGenerationRunner(projectionDirectory, executionId, dependencies = {}) {
23
21
  const openSync = dependencies.openSync ?? fs.openSync;
24
22
  const closeSync = dependencies.closeSync ?? fs.closeSync;
25
23
  const absoluteDirectory = path.resolve(projectionDirectory);
@@ -29,18 +27,21 @@ export function spawnProjectionGenerationRunner(projectionDirectory, executionId
29
27
  const logPath = executionStdioLogPath(absoluteDirectory, executionId);
30
28
  const logFd = openSync(logPath, "a");
31
29
  try {
32
- const child = spawn(command, args, {
33
- detached: true,
34
- stdio: ["ignore", logFd, logFd, "ipc"],
35
- env: { ...process.env, [PROJECTION_RUNNER_BOOTSTRAP_ENV]: "1" },
36
- });
37
- if (child.pid === undefined) {
38
- throw new Error("projection runner did not expose a process id");
39
- }
40
- child.unref();
41
- return child;
30
+ return {
31
+ launch: {
32
+ command,
33
+ args,
34
+ unref: true,
35
+ options: {
36
+ stdio: ["ignore", logFd, logFd, "ipc"],
37
+ env: { ...process.env, [RUN_CONTROL_BOOTSTRAP_ENV]: "1" },
38
+ },
39
+ },
40
+ close: () => closeSync(logFd),
41
+ };
42
42
  }
43
- finally {
43
+ catch (error) {
44
44
  closeSync(logFd);
45
+ throw error;
45
46
  }
46
47
  }