@astrosheep/keiyaku 2.9.12 → 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 (212) hide show
  1. package/README.md +22 -18
  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 +2 -2
  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/petition/handler.js +2 -1
  17. package/build/cli/commands/contract/petition/meta.js +2 -2
  18. package/build/cli/commands/contract/renew/handler.js +44 -3
  19. package/build/cli/commands/contract/renew/meta.js +5 -4
  20. package/build/cli/commands/metadata.js +12 -9
  21. package/build/cli/commands/projection/call/handler.js +1 -1
  22. package/build/cli/commands/projection/call/meta.js +1 -1
  23. package/build/cli/commands/projection/catalog.js +2 -2
  24. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  25. package/build/cli/commands/projection/fork/meta.js +12 -0
  26. package/build/cli/commands/projection/history/handler.js +46 -2
  27. package/build/cli/commands/projection/history/meta.js +3 -3
  28. package/build/cli/commands/projection/kill/meta.js +2 -2
  29. package/build/cli/commands/projection/status/handler.js +29 -13
  30. package/build/cli/commands/projection/status/meta.js +14 -3
  31. package/build/cli/commands/projection/tell/handler.js +8 -4
  32. package/build/cli/commands/projection/tell/meta.js +1 -1
  33. package/build/cli/commands/projection/wait/handler.js +45 -20
  34. package/build/cli/commands/projection/wait/meta.js +1 -1
  35. package/build/cli/commands/shared.js +1 -1
  36. package/build/cli/commands/system/catalog.js +2 -0
  37. package/build/cli/commands/task/add/handler.js +28 -12
  38. package/build/cli/commands/task/add/meta.js +18 -6
  39. package/build/cli/commands/task/catalog.js +2 -0
  40. package/build/cli/commands/task/compose/handler.js +2 -2
  41. package/build/cli/commands/task/compose/meta.js +2 -2
  42. package/build/cli/commands/task/doctor/handler.js +2 -3
  43. package/build/cli/commands/task/doctor/meta.js +2 -2
  44. package/build/cli/commands/task/done/meta.js +2 -2
  45. package/build/cli/commands/task/drop/handler.js +7 -2
  46. package/build/cli/commands/task/drop/meta.js +6 -3
  47. package/build/cli/commands/task/hold/meta.js +2 -2
  48. package/build/cli/commands/task/log/meta.js +2 -2
  49. package/build/cli/commands/task/ls/meta.js +2 -2
  50. package/build/cli/commands/task/note/handler.js +6 -0
  51. package/build/cli/commands/task/note/meta.js +11 -0
  52. package/build/cli/commands/task/resume/meta.js +2 -2
  53. package/build/cli/commands/task/shared.js +29 -15
  54. package/build/cli/commands/task/show/meta.js +2 -2
  55. package/build/cli/commands/task/start/meta.js +2 -2
  56. package/build/cli/commands/task/stop/meta.js +2 -2
  57. package/build/cli/commands/task/update/meta.js +3 -2
  58. package/build/cli/commands/verification/handler.js +43 -0
  59. package/build/cli/commands/verification/meta.js +13 -0
  60. package/build/cli/completion.js +16 -13
  61. package/build/cli/flags.js +43 -11
  62. package/build/cli/help.js +29 -30
  63. package/build/cli/index.js +6 -5
  64. package/build/cli/parse-flags.js +39 -0
  65. package/build/cli/parse-metadata.js +5 -2
  66. package/build/cli/parse-selectors.js +18 -0
  67. package/build/cli/parse.js +21 -8
  68. package/build/cli/render/arc.js +12 -3
  69. package/build/cli/render/audit.js +2 -2
  70. package/build/cli/render/call.js +4 -4
  71. package/build/cli/render/kanshi.js +122 -85
  72. package/build/cli/render/misc.js +2 -2
  73. package/build/cli/render/petition.js +16 -1
  74. package/build/cli/render/projection-history.js +17 -4
  75. package/build/cli/render/shared.js +5 -5
  76. package/build/cli/render/status-indicator.js +47 -0
  77. package/build/cli/render/status.js +106 -76
  78. package/build/cli/render/success-response.js +20 -5
  79. package/build/cli/render/task.js +6 -5
  80. package/build/cli/render/tell.js +10 -4
  81. package/build/cli/render/tool-command-normalization.js +137 -0
  82. package/build/cli/render/tool-presentation.js +2 -1
  83. package/build/cli/render/verification.js +23 -0
  84. package/build/cli/render/wait.js +101 -33
  85. package/build/cli/types.js +2 -1
  86. package/build/config/akuma-loader.js +21 -1
  87. package/build/config/settings/knobs.js +7 -1
  88. package/build/config/settings/schema.js +10 -5
  89. package/build/core/amend.js +102 -17
  90. package/build/core/arc.js +4 -4
  91. package/build/core/audit/candidate.js +10 -18
  92. package/build/core/audit/coordinates.js +12 -17
  93. package/build/core/audit/evidence.js +6 -4
  94. package/build/core/audit/facade.js +3 -3
  95. package/build/core/audit/report.js +1 -1
  96. package/build/core/bind-reconciliation.js +42 -23
  97. package/build/core/bind.js +182 -81
  98. package/build/core/call/call.js +6 -7
  99. package/build/core/call/context.js +43 -30
  100. package/build/core/call/execution.js +21 -6
  101. package/build/core/call/prompt.js +11 -42
  102. package/build/core/contract-carrier-runtime.js +209 -105
  103. package/build/core/contract-carrier.js +156 -73
  104. package/build/core/contract-view.js +42 -5
  105. package/build/core/contract.js +2 -1
  106. package/build/core/derived-replay.js +168 -35
  107. package/build/core/draft.js +9 -5
  108. package/build/core/forfeit.js +1 -1
  109. package/build/core/lifecycle-history.js +13 -0
  110. package/build/core/lifecycle-recovery.js +32 -27
  111. package/build/core/lifecycle-runner.js +14 -14
  112. package/build/core/log.js +1 -1
  113. package/build/core/outcome-base.js +2 -2
  114. package/build/core/projection/akuma-name.js +43 -0
  115. package/build/core/projection/generation/database.js +141 -79
  116. package/build/core/projection/generation/ledger.js +91 -0
  117. package/build/core/projection/generation/model.js +18 -13
  118. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  119. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  120. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  121. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  122. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  123. package/build/core/projection/generation/projection-generation-process.js +16 -15
  124. package/build/core/projection/generation/projection-generation-runner.js +125 -79
  125. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  126. package/build/core/projection/generation/protocol.js +68 -0
  127. package/build/core/projection/generation/store.js +17 -7
  128. package/build/core/projection/generation/transitions.js +14 -14
  129. package/build/core/projection/index.js +5 -5
  130. package/build/core/projection/projection-core.js +6 -2
  131. package/build/core/projection/projection-execution-observer.js +1 -1
  132. package/build/core/projection/projection-history.js +7 -7
  133. package/build/core/projection/projection-kill.js +15 -6
  134. package/build/core/projection/projection-life-observer.js +8 -8
  135. package/build/core/projection/projection-mint.js +6 -8
  136. package/build/core/projection/projection-status-observation.js +77 -8
  137. package/build/core/projection/projection-status.js +151 -4
  138. package/build/core/projection/projection-wait.js +46 -27
  139. package/build/core/projection/projection-wake.js +27 -89
  140. package/build/core/projection/tell/launch-store.js +1 -1
  141. package/build/core/projection/tell/store.js +1 -1
  142. package/build/core/renew-build.js +204 -74
  143. package/build/core/renew-plan.js +34 -9
  144. package/build/core/renew-rewrite.js +56 -15
  145. package/build/core/renew.js +39 -35
  146. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  147. package/build/core/repository-ledger/codec.js +11 -5
  148. package/build/core/repository-ledger/current-state-store.js +13 -16
  149. package/build/core/repository-ledger/fold-repository.js +5 -6
  150. package/build/core/repository-ledger/identity.js +60 -0
  151. package/build/core/repository-ledger/inventory.js +3 -3
  152. package/build/core/repository-ledger/read-model.js +54 -6
  153. package/build/core/repository-ledger/write-transaction.js +9 -4
  154. package/build/core/run-control/connection-bound-close.js +41 -0
  155. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  156. package/build/core/run-control/detached-process.js +14 -0
  157. package/build/core/run-control/index.js +6 -0
  158. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  159. package/build/core/run-control/runner-lease.js +172 -0
  160. package/build/core/{sqlite-process-lifetime-lock.js → run-control/sqlite-process-lifetime-lock.js} +4 -24
  161. package/build/core/seal.js +44 -32
  162. package/build/core/settlement/claim-delivery.js +22 -9
  163. package/build/core/settlement/claim.js +8 -6
  164. package/build/core/settlement/petition-claim-gates.js +9 -8
  165. package/build/core/settlement/petition-head-guard.js +5 -6
  166. package/build/core/settlement/petition-preview.js +21 -5
  167. package/build/core/settlement/petition.js +51 -17
  168. package/build/core/settlement/settlement.js +20 -15
  169. package/build/core/settlement/verdict.js +2 -2
  170. package/build/core/settlement/verification.js +492 -364
  171. package/build/core/status/board.js +184 -75
  172. package/build/core/status/drift.js +8 -10
  173. package/build/core/task/board.js +1 -0
  174. package/build/core/task/commands.js +25 -0
  175. package/build/core/task/compose.js +3 -2
  176. package/build/core/task/document.js +102 -39
  177. package/build/core/task/index.js +3 -3
  178. package/build/core/task/settlement-git.js +1 -4
  179. package/build/core/task/settlement-policy.js +27 -14
  180. package/build/core/task/source-board.js +1 -0
  181. package/build/core/task/task-bind-preparation.js +59 -9
  182. package/build/core/task/task-contract.js +2 -2
  183. package/build/core/task/task-store-repository.js +1 -0
  184. package/build/core/task/task.js +2 -2
  185. package/build/core/transcripts.js +4 -4
  186. package/build/core/verification-declaration.js +95 -0
  187. package/build/core/worktree-bootstrap.js +1 -1
  188. package/build/core/worktree-path.js +7 -11
  189. package/build/flow-error.js +9 -1
  190. package/build/generated/version.js +2 -2
  191. package/build/git/branches.js +41 -70
  192. package/build/git/commits.js +92 -21
  193. package/build/git/core.js +9 -161
  194. package/build/git/diff/preview.js +4 -4
  195. package/build/git/diff/read.js +4 -4
  196. package/build/git/diff/structured.js +5 -5
  197. package/build/git/process.js +229 -0
  198. package/build/git/refs.js +30 -5
  199. package/build/git/staging.js +39 -115
  200. package/build/git/streaming-batch.js +9 -16
  201. package/build/git/worktree.js +104 -91
  202. package/build/index.js +7 -0
  203. package/package.json +1 -1
  204. package/skills/keiyaku-akuma/SKILL.md +21 -20
  205. package/skills/keiyaku-task/SKILL.md +38 -4
  206. package/skills/keiyaku-workflow/SKILL.md +51 -11
  207. package/build/cli/commands/projection/revive/meta.js +0 -12
  208. package/build/core/projection/projection-life-protocol.js +0 -115
  209. package/build/core/projection/projection-runner-lock.js +0 -211
  210. package/build/core/projection/tell/database.js +0 -127
  211. package/build/core/settlement/verification-coordination.js +0 -305
  212. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
3
+ const FORBIDDEN_CODE_POINT = /[\u0000-\u001f\u007f-\u009f\uD800-\uDFFF]/u;
4
+ const FORBIDDEN_PORTABLE_PATH_CHARACTER = /[\\/<>:"|?*]/u;
5
+ const MAX_AKUMA_NAME_BYTES = 255;
6
+ export function canonicalizeAkumaName(value) {
7
+ return value.normalize("NFC");
8
+ }
9
+ export function akumaNameProblem(value) {
10
+ if (value.length === 0)
11
+ return "must not be empty.";
12
+ if (value !== canonicalizeAkumaName(value))
13
+ return "must use Unicode NFC normalization.";
14
+ if (value === "." || value === ".." || value.startsWith(".")) {
15
+ return "must not be a dot path or hidden filesystem name.";
16
+ }
17
+ if (FORBIDDEN_CODE_POINT.test(value))
18
+ return "must not contain control characters or unpaired surrogates.";
19
+ if (/\p{White_Space}/u.test(value))
20
+ return "must not contain whitespace.";
21
+ if (FORBIDDEN_PORTABLE_PATH_CHARACTER.test(value))
22
+ return "must not contain portable filesystem separators or reserved punctuation.";
23
+ if (/[. ]$/u.test(value))
24
+ return "must not end with a dot or space.";
25
+ if (WINDOWS_RESERVED_NAME.test(value))
26
+ return "must not be a reserved Windows filename.";
27
+ if (Buffer.byteLength(value, "utf8") > MAX_AKUMA_NAME_BYTES) {
28
+ return `must contain at most ${MAX_AKUMA_NAME_BYTES} UTF-8 bytes.`;
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Akuma identity is exact NFC text. Case, variation selectors, ZWJ sequences,
34
+ * and visually confusable code points are intentionally not folded.
35
+ */
36
+ export const akumaNameSchema = z.string().superRefine((value, ctx) => {
37
+ const problem = akumaNameProblem(value);
38
+ if (problem !== null)
39
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });
40
+ });
41
+ export function isAkumaName(value) {
42
+ return akumaNameProblem(value) === null;
43
+ }
@@ -1,20 +1,22 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
+ import { ProjectionStateError } from "../../atomic-publish.js";
4
5
  import { configureHeartBusyTimeout } from "../heart.js";
5
6
  import { ProjectionGenerationStoreError, parseStoredRow, validateHistory, } from "./model.js";
7
+ import { ProjectionGenerationLedger } from "./ledger.js";
6
8
  export const PROJECTION_GENERATION_STORE_FILE = "heart";
7
- export const PROJECTION_GENERATION_SCHEMA_VERSION = 4;
8
- const SCHEMA_TABLE_SQL = `
9
+ export const PROJECTION_GENERATION_SCHEMA_VERSION = 6;
10
+ const GENERATION_SCHEMA_SQL = `
9
11
  CREATE TABLE generation_schema (
10
12
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
11
13
  version INTEGER NOT NULL CHECK (version = ${PROJECTION_GENERATION_SCHEMA_VERSION})
12
14
  ) STRICT
13
15
  `;
14
- const RECORD_TABLE_SQL = `
16
+ const GENERATION_RECORD_SQL = `
15
17
  CREATE TABLE generation_records (
16
18
  seq INTEGER PRIMARY KEY CHECK (seq > 0),
17
- kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'kill-intent', 'interrupt-intent', 'verdict')),
19
+ activity_kind TEXT NOT NULL CHECK (activity_kind IN ('launch', 'adoption', 'kill-intent', 'interrupt-intent', 'verdict')),
18
20
  execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
19
21
  facts_json TEXT NOT NULL CHECK (json_valid(facts_json))
20
22
  ) STRICT
@@ -92,6 +94,34 @@ const TELL_LAUNCH_COORDINATE_TABLE_SQL = `
92
94
  ) STRICT
93
95
  `;
94
96
  const closedDatabases = new WeakSet();
97
+ function projectionHeartPath(projectionDirectory) {
98
+ return path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
99
+ }
100
+ function openHeartConnection(databasePath, readOnly, foreignKeys = false) {
101
+ const database = new DatabaseSync(databasePath, {
102
+ readOnly,
103
+ ...(foreignKeys ? { enableForeignKeyConstraints: true } : {}),
104
+ enableDoubleQuotedStringLiterals: false,
105
+ allowExtension: false,
106
+ });
107
+ try {
108
+ configureHeartBusyTimeout(database);
109
+ if (!readOnly) {
110
+ database.exec("PRAGMA journal_mode = DELETE");
111
+ database.exec("PRAGMA synchronous = FULL");
112
+ database.exec("PRAGMA foreign_keys = ON");
113
+ database.exec("PRAGMA trusted_schema = OFF");
114
+ }
115
+ return database;
116
+ }
117
+ catch (error) {
118
+ try {
119
+ database.close();
120
+ }
121
+ catch { /* preserve configuration failure */ }
122
+ throw error;
123
+ }
124
+ }
95
125
  /** Classify only proved missing schema evidence as historical incompatibility. */
96
126
  export function projectionGenerationStoreInspectionError(databasePath, error) {
97
127
  if (error instanceof ProjectionGenerationStoreError)
@@ -153,14 +183,16 @@ function validateSchemaShape(database) {
153
183
  throw new ProjectionGenerationStoreError(`projection generation schema contains unexpected objects: ${signature || "none"}`, { observation: "incompatible" });
154
184
  }
155
185
  const metadata = database.prepare("SELECT singleton, version FROM generation_schema").all();
156
- if (metadata.length !== 1 || Number(metadata[0].singleton) !== 1 || Number(metadata[0].version) !== PROJECTION_GENERATION_SCHEMA_VERSION) {
186
+ if (metadata.length !== 1
187
+ || Number(metadata[0].singleton) !== 1
188
+ || Number(metadata[0].version) !== PROJECTION_GENERATION_SCHEMA_VERSION) {
157
189
  throw new ProjectionGenerationStoreError("projection generation schema metadata is malformed", { observation: "incompatible" });
158
190
  }
159
- const recordColumns = database.prepare("PRAGMA table_info(generation_records)").all();
160
- const columnSignature = recordColumns
191
+ const generationColumns = database.prepare("PRAGMA table_info(generation_records)").all();
192
+ const generationSignature = generationColumns
161
193
  .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
162
194
  .join(",");
163
- if (columnSignature !== "seq:INTEGER:0:1,kind:TEXT:1:0,execution_id:TEXT:1:0,facts_json:TEXT:1:0") {
195
+ if (generationSignature !== "seq:INTEGER:0:1,activity_kind:TEXT:1:0,execution_id:TEXT:1:0,facts_json:TEXT:1:0") {
164
196
  throw new ProjectionGenerationStoreError("projection generation record schema is malformed", { observation: "incompatible" });
165
197
  }
166
198
  const identityColumns = database.prepare("PRAGMA table_info(projection_identity)").all();
@@ -228,8 +260,10 @@ function initializeOrValidateSchema(database) {
228
260
  ORDER BY name
229
261
  `).all();
230
262
  if (version === 0 && objects.length === 0) {
231
- database.exec(SCHEMA_TABLE_SQL);
232
- database.exec(RECORD_TABLE_SQL);
263
+ database.exec(GENERATION_SCHEMA_SQL);
264
+ database.exec(GENERATION_RECORD_SQL);
265
+ database.prepare("INSERT INTO generation_schema (singleton, version) VALUES (1, ?)")
266
+ .run(PROJECTION_GENERATION_SCHEMA_VERSION);
233
267
  database.exec(IDENTITY_TABLE_SQL);
234
268
  database.exec(TELL_TABLE_SQL);
235
269
  database.exec(TELL_RECEIPT_TABLE_SQL);
@@ -237,8 +271,6 @@ function initializeOrValidateSchema(database) {
237
271
  database.exec(TELL_LAUNCH_BATCH_TABLE_SQL);
238
272
  database.exec(TELL_LAUNCH_ADMISSION_TABLE_SQL);
239
273
  database.exec(TELL_LAUNCH_COORDINATE_TABLE_SQL);
240
- database.prepare("INSERT INTO generation_schema (singleton, version) VALUES (1, ?)")
241
- .run(PROJECTION_GENERATION_SCHEMA_VERSION);
242
274
  database.exec(`PRAGMA user_version = ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
243
275
  }
244
276
  else if (version !== PROJECTION_GENERATION_SCHEMA_VERSION) {
@@ -256,22 +288,10 @@ export function openMutableDatabase(projectionDirectory) {
256
288
  const stat = fs.statSync(projectionDirectory);
257
289
  if (!stat.isDirectory())
258
290
  throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
259
- const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
291
+ const databasePath = projectionHeartPath(projectionDirectory);
260
292
  let database;
261
293
  try {
262
- database = new DatabaseSync(databasePath, {
263
- enableForeignKeyConstraints: true,
264
- enableDoubleQuotedStringLiterals: false,
265
- allowExtension: false,
266
- });
267
- configureHeartBusyTimeout(database);
268
- // Rollback journal keeps observers genuinely read-only. A read-only WAL
269
- // connection creates/updates -wal and -shm coordination files, while
270
- // immutable WAL reads can miss concurrently committed facts.
271
- database.exec("PRAGMA journal_mode = DELETE");
272
- database.exec("PRAGMA synchronous = FULL");
273
- database.exec("PRAGMA foreign_keys = ON");
274
- database.exec("PRAGMA trusted_schema = OFF");
294
+ database = openHeartConnection(databasePath, false, true);
275
295
  }
276
296
  catch (error) {
277
297
  try {
@@ -282,7 +302,7 @@ export function openMutableDatabase(projectionDirectory) {
282
302
  }
283
303
  try {
284
304
  initializeOrValidateSchema(database);
285
- readValidatedHistory(database);
305
+ readValidatedHistory(database, false);
286
306
  return { databasePath, database };
287
307
  }
288
308
  catch (error) {
@@ -296,7 +316,7 @@ export function openReadOnlyDatabase(projectionDirectory) {
296
316
  const stat = fs.statSync(projectionDirectory);
297
317
  if (!stat.isDirectory())
298
318
  throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
299
- const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
319
+ const databasePath = projectionHeartPath(projectionDirectory);
300
320
  try {
301
321
  if (!fs.lstatSync(databasePath).isFile()) {
302
322
  throw new ProjectionGenerationStoreError("projection generation store must be a regular file", { observation: "incompatible" });
@@ -307,14 +327,9 @@ export function openReadOnlyDatabase(projectionDirectory) {
307
327
  }
308
328
  let database;
309
329
  try {
310
- database = new DatabaseSync(databasePath, {
311
- readOnly: true,
312
- enableDoubleQuotedStringLiterals: false,
313
- allowExtension: false,
314
- });
315
- configureHeartBusyTimeout(database);
330
+ database = openHeartConnection(databasePath, true);
316
331
  validateSchemaShape(database);
317
- readValidatedHistory(database);
332
+ readValidatedHistory(database, true);
318
333
  return { databasePath, database };
319
334
  }
320
335
  catch (error) {
@@ -325,13 +340,14 @@ export function openReadOnlyDatabase(projectionDirectory) {
325
340
  throw projectionGenerationStoreReadError(databasePath, error);
326
341
  }
327
342
  }
328
- /** Read the validated heart through one consistent SQLite snapshot. */
329
- export function withGenerationReadSnapshot(projectionDirectory, read) {
330
- const { database } = openReadOnlyDatabase(projectionDirectory);
343
+ function withProjectionHeartOperation(open, mode, operation) {
344
+ const database = open();
331
345
  try {
332
- database.exec("BEGIN");
346
+ if (mode === "read")
347
+ return operation(database);
348
+ database.exec(mode === "snapshot" ? "BEGIN" : "BEGIN IMMEDIATE");
333
349
  try {
334
- const result = read(database);
350
+ const result = operation(database);
335
351
  database.exec("COMMIT");
336
352
  return result;
337
353
  }
@@ -344,64 +360,110 @@ export function withGenerationReadSnapshot(projectionDirectory, read) {
344
360
  closeDatabase(database);
345
361
  }
346
362
  }
363
+ /** Read the validated heart through one consistent SQLite snapshot. */
364
+ export function withGenerationReadSnapshot(projectionDirectory, read) {
365
+ return withProjectionHeartOperation(() => openReadOnlyDatabase(projectionDirectory).database, "snapshot", read);
366
+ }
367
+ function openProjectionHeartDatabase(projectionDirectory, readOnly) {
368
+ const databasePath = projectionHeartPath(projectionDirectory);
369
+ if (!fs.lstatSync(databasePath).isFile()) {
370
+ throw new ProjectionStateError("projection heart must be a regular file");
371
+ }
372
+ return openHeartConnection(databasePath, readOnly, true);
373
+ }
374
+ export function withProjectionHeartRead(projectionDirectory, read) {
375
+ return withProjectionHeartOperation(() => openProjectionHeartDatabase(projectionDirectory, true), "read", read);
376
+ }
377
+ export function withProjectionHeartReadSnapshot(projectionDirectory, read) {
378
+ return withProjectionHeartOperation(() => openProjectionHeartDatabase(projectionDirectory, true), "snapshot", read);
379
+ }
380
+ export function withProjectionHeartWrite(projectionDirectory, write) {
381
+ return withProjectionHeartOperation(() => openProjectionHeartDatabase(projectionDirectory, false), "write", write);
382
+ }
383
+ export function openProjectionTellObserver(projectionDirectory) {
384
+ const database = openProjectionHeartDatabase(projectionDirectory, true);
385
+ let closed = false;
386
+ let observedDataVersion;
387
+ const assertOpen = () => {
388
+ if (closed)
389
+ throw new ProjectionStateError("projection tell observer is closed");
390
+ };
391
+ const readDataVersion = () => {
392
+ assertOpen();
393
+ const row = database.prepare("PRAGMA data_version").get();
394
+ const raw = row?.data_version;
395
+ const value = typeof raw === "bigint" ? Number(raw) : raw;
396
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
397
+ throw new ProjectionStateError("PRAGMA data_version returned an invalid value");
398
+ }
399
+ return value;
400
+ };
401
+ return {
402
+ hasDataVersionChanged() {
403
+ const current = readDataVersion();
404
+ if (current === observedDataVersion)
405
+ return false;
406
+ observedDataVersion = current;
407
+ return true;
408
+ },
409
+ hasInbox() {
410
+ assertOpen();
411
+ return database.prepare(`
412
+ SELECT 1
413
+ FROM projection_tells
414
+ WHERE state = 'inbox'
415
+ LIMIT 1
416
+ `).get() !== undefined;
417
+ },
418
+ close() {
419
+ if (closed)
420
+ return;
421
+ closed = true;
422
+ closeDatabase(database);
423
+ },
424
+ };
425
+ }
347
426
  export function assertDatabaseOpen(database) {
348
427
  if (closedDatabases.has(database))
349
428
  throw new ProjectionGenerationStoreError("projection generation store is closed");
350
429
  }
351
- export function readValidatedHistory(database) {
352
- const rows = database.prepare(`
353
- SELECT seq, kind, execution_id, facts_json
354
- FROM generation_records
355
- ORDER BY seq
356
- `).all();
357
- const records = rows.map(parseStoredRow);
430
+ export function connectionBoundGenerationStore(database, readOnly) {
431
+ return new ProjectionGenerationLedger(database, readOnly);
432
+ }
433
+ export function readValidatedHistory(database, readOnly) {
434
+ const records = connectionBoundGenerationStore(database, readOnly).readRecords().map((record) => parseStoredRow({
435
+ seq: record.seq,
436
+ kind: record.kind,
437
+ execution_id: record.executionId,
438
+ facts_json: JSON.stringify(record.facts),
439
+ }));
358
440
  return { records, current: validateHistory(records) };
359
441
  }
360
442
  export function appendRecord(database, kind, executionId, facts, seq) {
361
- const encoded = JSON.stringify(facts);
362
- database.prepare(`
363
- INSERT INTO generation_records (seq, kind, execution_id, facts_json)
364
- VALUES (?, ?, ?, ?)
365
- `).run(seq, kind, executionId, encoded);
366
- return { seq, kind, executionId, facts: JSON.parse(encoded) };
443
+ const record = connectionBoundGenerationStore(database, false)
444
+ .appendRecord(kind, executionId, facts, seq);
445
+ return {
446
+ seq: record.seq,
447
+ kind: record.kind,
448
+ executionId: record.executionId,
449
+ facts: record.facts,
450
+ };
367
451
  }
368
452
  export function writeTransition(database, readOnly, run) {
369
453
  assertDatabaseOpen(database);
370
- if (readOnly) {
371
- throw new ProjectionGenerationStoreError("projection generation store is read-only");
372
- }
373
- if (database.isTransaction)
374
- return run();
375
- database.exec("BEGIN IMMEDIATE");
376
- try {
377
- const result = run();
378
- database.exec("COMMIT");
379
- return result;
380
- }
381
- catch (error) {
382
- rollbackQuietly(database);
383
- throw error;
384
- }
454
+ return connectionBoundGenerationStore(database, readOnly).write(run);
385
455
  }
386
456
  export function beginGenerationWriteTransaction(database, readOnly) {
387
457
  assertDatabaseOpen(database);
388
- if (readOnly)
389
- throw new ProjectionGenerationStoreError("projection generation store is read-only");
390
- if (database.isTransaction)
391
- throw new ProjectionGenerationStoreError("projection generation write transaction is already open");
392
- database.exec("BEGIN IMMEDIATE");
458
+ connectionBoundGenerationStore(database, readOnly).beginWriteTransaction();
393
459
  }
394
460
  export function commitGenerationWriteTransaction(database) {
395
461
  assertDatabaseOpen(database);
396
- if (!database.isTransaction)
397
- throw new ProjectionGenerationStoreError("projection generation write transaction is not open");
398
- database.exec("COMMIT");
462
+ connectionBoundGenerationStore(database, false).commitWriteTransaction();
399
463
  }
400
464
  export function rollbackGenerationWriteTransaction(database) {
401
465
  assertDatabaseOpen(database);
402
- if (!database.isTransaction)
403
- return;
404
- rollbackQuietly(database);
466
+ connectionBoundGenerationStore(database, false).rollbackWriteTransaction();
405
467
  }
406
468
  export function closeDatabase(database) {
407
469
  if (closedDatabases.has(database))
@@ -0,0 +1,91 @@
1
+ import { assertExecutionId, assertPositiveSafeInteger, cloneFacts, ProjectionGenerationStoreError, } from "./model.js";
2
+ const RECORD_KINDS = new Set([
3
+ "launch", "adoption", "kill-intent", "interrupt-intent", "verdict",
4
+ ]);
5
+ /** The projection-owned persistence adapter for generation facts in heart. */
6
+ export class ProjectionGenerationLedger {
7
+ #database;
8
+ #readOnly;
9
+ constructor(database, readOnly) {
10
+ this.#database = database;
11
+ this.#readOnly = readOnly;
12
+ }
13
+ readRecords() {
14
+ const rows = this.#database.prepare(`
15
+ SELECT seq, activity_kind, execution_id, facts_json
16
+ FROM generation_records
17
+ ORDER BY seq
18
+ `).all();
19
+ return rows.map((row) => {
20
+ const seq = assertPositiveSafeInteger(Number(row.seq), "generation row seq");
21
+ if (typeof row.activity_kind !== "string" || !RECORD_KINDS.has(row.activity_kind)) {
22
+ throw new ProjectionGenerationStoreError(`generation row ${seq} has unknown kind '${String(row.activity_kind)}'`);
23
+ }
24
+ if (typeof row.execution_id !== "string")
25
+ throw new ProjectionGenerationStoreError(`generation row ${seq} has invalid execution_id`);
26
+ const executionId = assertExecutionId(row.execution_id);
27
+ if (typeof row.facts_json !== "string")
28
+ throw new ProjectionGenerationStoreError(`generation row ${seq} facts_json must be text`);
29
+ let facts;
30
+ try {
31
+ facts = cloneFacts(JSON.parse(row.facts_json), `generation row ${seq} facts`);
32
+ }
33
+ catch (cause) {
34
+ if (cause instanceof Error && cause.message.startsWith(`generation row ${seq}`))
35
+ throw cause;
36
+ throw new ProjectionGenerationStoreError(`generation row ${seq} facts_json is invalid JSON`, { cause });
37
+ }
38
+ return { seq, kind: row.activity_kind, executionId, facts };
39
+ });
40
+ }
41
+ appendRecord(kind, executionId, facts, seq) {
42
+ if (this.#readOnly)
43
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
44
+ const encoded = JSON.stringify(facts);
45
+ this.#database.prepare(`
46
+ INSERT INTO generation_records (seq, activity_kind, execution_id, facts_json)
47
+ VALUES (?, ?, ?, ?)
48
+ `).run(seq, kind, executionId, encoded);
49
+ return { seq, kind, executionId, facts: JSON.parse(encoded) };
50
+ }
51
+ write(run) {
52
+ if (this.#readOnly)
53
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
54
+ if (this.#database.isTransaction)
55
+ return run();
56
+ this.#database.exec("BEGIN IMMEDIATE");
57
+ try {
58
+ const result = run();
59
+ this.#database.exec("COMMIT");
60
+ return result;
61
+ }
62
+ catch (error) {
63
+ this.rollback();
64
+ throw error;
65
+ }
66
+ }
67
+ beginWriteTransaction() {
68
+ if (this.#readOnly)
69
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
70
+ if (this.#database.isTransaction)
71
+ throw new ProjectionGenerationStoreError("projection generation write transaction is already open");
72
+ this.#database.exec("BEGIN IMMEDIATE");
73
+ }
74
+ commitWriteTransaction() {
75
+ if (!this.#database.isTransaction)
76
+ throw new ProjectionGenerationStoreError("projection generation write transaction is not open");
77
+ this.#database.exec("COMMIT");
78
+ }
79
+ rollbackWriteTransaction() {
80
+ if (this.#database.isTransaction)
81
+ this.rollback();
82
+ }
83
+ rollback() {
84
+ try {
85
+ this.#database.exec("ROLLBACK");
86
+ }
87
+ catch {
88
+ // Preserve the failure that caused rollback.
89
+ }
90
+ }
91
+ }
@@ -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) {
@@ -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
+ }