@astrosheep/keiyaku 4.6.19 → 4.6.21

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 (102) hide show
  1. package/README.md +7 -3
  2. package/build/plugins/pi/README.md +111 -0
  3. package/build/plugins/pi/akuma-tools.ts +310 -0
  4. package/build/plugins/pi/keiyaku.ts +2 -0
  5. package/build/src/akuma/akuma-call.d.ts +4 -1
  6. package/build/src/akuma/akuma-call.js +24 -7
  7. package/build/src/akuma/akuma-instance.d.ts +28 -8
  8. package/build/src/akuma/akuma-instance.js +85 -42
  9. package/build/src/akuma/akuma-owner.d.ts +18 -9
  10. package/build/src/akuma/akuma-owner.js +31 -25
  11. package/build/src/akuma/akuma-selection-execution.d.ts +4 -1
  12. package/build/src/akuma/akuma-selection-execution.js +8 -4
  13. package/build/src/akuma/akuma.d.ts +17 -0
  14. package/build/src/akuma/body-launch.d.ts +107 -0
  15. package/build/src/akuma/body-launch.js +57 -0
  16. package/build/src/akuma/body-supervisor.d.ts +7 -6
  17. package/build/src/akuma/body-supervisor.js +17 -12
  18. package/build/src/akuma/body.d.ts +13 -94
  19. package/build/src/akuma/body.js +124 -296
  20. package/build/src/akuma/call-input.d.ts +2 -0
  21. package/build/src/akuma/call-input.js +21 -1
  22. package/build/src/akuma/call-request.d.ts +6 -2
  23. package/build/src/akuma/call-request.js +9 -1
  24. package/build/src/akuma/detached-body.d.ts +9 -0
  25. package/build/src/akuma/detached-body.js +94 -0
  26. package/build/src/akuma/execution-instructions.d.ts +8 -0
  27. package/build/src/akuma/execution-instructions.js +17 -0
  28. package/build/src/akuma/heart/facts.d.ts +21 -1
  29. package/build/src/akuma/heart/facts.js +10 -0
  30. package/build/src/akuma/heart/index.d.ts +16 -1
  31. package/build/src/akuma/heart/index.js +95 -30
  32. package/build/src/akuma/heart/rows.d.ts +14 -2
  33. package/build/src/akuma/heart/rows.js +48 -4
  34. package/build/src/akuma/heart/schema.d.ts +4 -1
  35. package/build/src/akuma/heart/schema.js +87 -30
  36. package/build/src/akuma/heart/soul.js +4 -0
  37. package/build/src/akuma/heart/storage.d.ts +1 -0
  38. package/build/src/akuma/heart/storage.js +59 -15
  39. package/build/src/akuma/heart/timeline.d.ts +6 -4
  40. package/build/src/akuma/heart/timeline.js +14 -4
  41. package/build/src/akuma/in-process-body.d.ts +5 -0
  42. package/build/src/akuma/in-process-body.js +57 -0
  43. package/build/src/akuma/index.d.ts +1 -1
  44. package/build/src/akuma/projection.d.ts +44 -1
  45. package/build/src/akuma/projection.js +13 -1
  46. package/build/src/akuma/provider.d.ts +4 -0
  47. package/build/src/akuma/providers/acp/core.js +1 -1
  48. package/build/src/akuma/providers/acp/index.js +3 -0
  49. package/build/src/akuma/providers/claude/index.js +8 -5
  50. package/build/src/akuma/providers/codex-app-server/configuration.d.ts +5 -0
  51. package/build/src/akuma/providers/codex-app-server/configuration.js +72 -0
  52. package/build/src/akuma/providers/codex-app-server/index.js +7 -10
  53. package/build/src/akuma/providers/execution-environment.d.ts +8 -2
  54. package/build/src/akuma/providers/execution-environment.js +10 -1
  55. package/build/src/akuma/providers/grok-build/index.js +23 -16
  56. package/build/src/akuma/providers/index.d.ts +1 -0
  57. package/build/src/akuma/providers/index.js +8 -0
  58. package/build/src/akuma/providers/opencode-sdk/index.js +9 -0
  59. package/build/src/akuma/providers/opencode-sdk/session.d.ts +6 -0
  60. package/build/src/akuma/providers/opencode-sdk/session.js +4 -3
  61. package/build/src/akuma/providers/paseo/index.js +8 -2
  62. package/build/src/akuma/providers/pi/index.js +27 -10
  63. package/build/src/akuma/publication.d.ts +4 -3
  64. package/build/src/akuma/publication.js +21 -18
  65. package/build/src/akuma/selection-observation.d.ts +51 -0
  66. package/build/src/akuma/selection-owner-port.d.ts +2 -1
  67. package/build/src/akuma/selection-owner-port.js +4 -3
  68. package/build/src/akuma/turn-drive.d.ts +4 -2
  69. package/build/src/akuma/turn-drive.js +58 -21
  70. package/build/src/akuma-body.d.ts +0 -10
  71. package/build/src/akuma-body.js +13 -118
  72. package/build/src/body-host.d.ts +28 -0
  73. package/build/src/body-host.js +86 -0
  74. package/build/src/body-requests.d.ts +13 -0
  75. package/build/src/body-requests.js +131 -0
  76. package/build/src/cli/commands/akuma-grammar.d.ts +8 -1
  77. package/build/src/cli/commands/akuma-grammar.js +36 -6
  78. package/build/src/cli/commands/akuma.js +11 -0
  79. package/build/src/cli/commands/report.d.ts +6 -0
  80. package/build/src/cli/commands/report.js +31 -0
  81. package/build/src/cli/parse.js +1 -0
  82. package/build/src/cli/render/akuma-activity.js +27 -17
  83. package/build/src/cli/render/terminal.d.ts +5 -0
  84. package/build/src/cli/render/terminal.js +7 -0
  85. package/build/src/cli/runtime.js +4 -1
  86. package/build/src/index.d.ts +3 -1
  87. package/build/src/index.js +1 -0
  88. package/build/src/library/akuma-creation.d.ts +7 -3
  89. package/build/src/library/akuma-creation.js +23 -11
  90. package/build/src/library/akumas.d.ts +4 -1
  91. package/build/src/library/akumas.js +13 -5
  92. package/build/src/library/selection.d.ts +2 -1
  93. package/build/src/library/selection.js +6 -3
  94. package/build/src/plugin/public.d.ts +11 -0
  95. package/build/src/plugin/runtime.js +1 -0
  96. package/build/src/runtime/proc/windows-launch.exe +0 -0
  97. package/package.json +3 -3
  98. package/plugins/paseo/plugin/server/keiyaku.ts +1 -0
  99. package/plugins/paseo/plugin/server/mirror.ts +2 -2
  100. package/plugins/pi/README.md +111 -0
  101. package/plugins/pi/akuma-tools.ts +310 -0
  102. package/plugins/pi/keiyaku.ts +2 -0
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { providerOptionsSchema, withoutModel } from "../provider-recipe.js";
3
3
  import { resumeCoordinateSchema } from "../coordinate.js";
4
- import { HeartAuthorityCorruptionError } from "./facts.js";
4
+ import { HeartAuthorityCorruptionError, reportFactSchema } from "./facts.js";
5
5
  import { decodeResumeCoordinate, encodeResumeCoordinate as encodeCoordinate } from "../coordinate.js";
6
6
  import { agentEventSchema } from "./activity-schema.js";
7
7
  const sessionText = z.string().refine((value) => value.trim() !== "");
@@ -80,6 +80,8 @@ function decodeTurnOutcome(row) {
80
80
  ...(row.session_json === null ? {} : { session: resumeCoordinate(parsed(row.session_json)) }),
81
81
  };
82
82
  }
83
+ if (row.outcome === "canceled")
84
+ return { kind: "canceled" };
83
85
  return { kind: "failed", diagnostic: row.diagnostic };
84
86
  }
85
87
  export function decodeTurnRow(row) {
@@ -105,6 +107,16 @@ export function decodeTurnRow(row) {
105
107
  export function decodeCallRow(row) {
106
108
  return { kind: "call", sequence: row.sequence, turnSequence: row.turn_sequence, body: row.body, at: row.at };
107
109
  }
110
+ export function decodeReportRow(row) {
111
+ return reportFactSchema.parse({
112
+ kind: "report",
113
+ sequence: row.sequence,
114
+ bodySequence: row.body_sequence,
115
+ turnSequence: row.turn_sequence,
116
+ text: row.text,
117
+ at: row.at,
118
+ });
119
+ }
108
120
  export function encodeActivityEvent(event) {
109
121
  return json(agentEventSchema.parse(event));
110
122
  }
@@ -149,6 +161,14 @@ export function insertSessionFact(database, input) {
149
161
  .run(...encodeSessionRow(input));
150
162
  return Number(result.lastInsertRowid);
151
163
  }
164
+ export function insertReportFact(database, input) {
165
+ const sequence = Number(database.prepare("INSERT INTO timeline(kind) VALUES ('report')").run().lastInsertRowid);
166
+ const fact = reportFactSchema.parse({ kind: "report", sequence, ...input });
167
+ database
168
+ .prepare("INSERT INTO reports(sequence, body_sequence, turn_sequence, text, at) VALUES (?, ?, ?, ?, ?)")
169
+ .run(sequence, input.bodySequence, input.turnSequence, input.text, input.at);
170
+ return fact;
171
+ }
152
172
  export function insertActivityFact(database, input) {
153
173
  const sequence = Number(database.prepare("INSERT INTO timeline(kind) VALUES ('activity')").run().lastInsertRowid);
154
174
  database
@@ -231,6 +251,11 @@ export function pauseFact(database) {
231
251
  ...(value.frontierTellId === undefined ? {} : { frontierTellId: value.frontierTellId }),
232
252
  };
233
253
  }
254
+ export function hasBodyHandoffControl(database, bodySequence) {
255
+ return (killFactForBody(database, bodySequence) !== null ||
256
+ stopFact(database)?.bodySequence === bodySequence ||
257
+ pauseFact(database)?.bodySequence === bodySequence);
258
+ }
234
259
  export function bodyFact(database, sequence) {
235
260
  const row = database
236
261
  .prepare(`SELECT sequence, leash_taken_at, hung_diagnostic, hung_at, end, ended_at FROM bodies WHERE sequence = ?`)
@@ -287,6 +312,20 @@ export function insertTurnStartFact(database, input) {
287
312
  ...(input.schemaJson === undefined ? {} : { schemaJson: input.schemaJson }),
288
313
  };
289
314
  }
315
+ export function openTurnsForBody(database, bodySequence) {
316
+ return database
317
+ .prepare("SELECT sequence FROM turns WHERE body_sequence = ? AND end_sequence IS NULL ORDER BY sequence")
318
+ .all(bodySequence)
319
+ .map((row) => Number(row.sequence));
320
+ }
321
+ export function terminalTurnForTell(database, tellId) {
322
+ const row = database
323
+ .prepare(`SELECT t.sequence FROM tell_bindings b
324
+ JOIN turns t ON t.sequence = b.turn_sequence
325
+ WHERE b.tell_id = ? AND t.end_sequence IS NOT NULL ORDER BY b.sequence LIMIT 1`)
326
+ .get(tellId);
327
+ return row === undefined ? null : turnFact(database, Number(row.sequence));
328
+ }
290
329
  export function insertTurnEndFact(database, input) {
291
330
  const sequence = Number(database.prepare("INSERT INTO timeline(kind) VALUES ('turn-end')").run().lastInsertRowid);
292
331
  const result = input.outcome.kind === "answered"
@@ -300,10 +339,15 @@ export function insertTurnEndFact(database, input) {
300
339
  session_json = ?, answer = ?, diagnostic = ?, completed_at = ?
301
340
  WHERE sequence = ? AND end_sequence IS NULL`)
302
341
  .run(sequence, input.outcome.historyId ?? null, input.outcome.session === undefined ? null : encodeResumeCoordinate(input.outcome.session), input.outcome.answer, input.outcome.diagnostic, input.completedAt, input.turnSequence)
303
- : database
304
- .prepare(`UPDATE turns SET end_sequence = ?, outcome = 'failed', diagnostic = ?, completed_at = ?
342
+ : input.outcome.kind === "canceled"
343
+ ? database
344
+ .prepare(`UPDATE turns SET end_sequence = ?, outcome = 'canceled', completed_at = ?
345
+ WHERE sequence = ? AND end_sequence IS NULL`)
346
+ .run(sequence, input.completedAt, input.turnSequence)
347
+ : database
348
+ .prepare(`UPDATE turns SET end_sequence = ?, outcome = 'failed', diagnostic = ?, completed_at = ?
305
349
  WHERE sequence = ? AND end_sequence IS NULL`)
306
- .run(sequence, input.outcome.diagnostic, input.completedAt, input.turnSequence);
350
+ .run(sequence, input.outcome.diagnostic, input.completedAt, input.turnSequence);
307
351
  if (result.changes !== 1)
308
352
  throw new Error(`Akuma Turn ${input.turnSequence} is not open`);
309
353
  return { ...input, kind: "turn-end", sequence };
@@ -1,6 +1,9 @@
1
1
  import type { DatabaseSync } from "node:sqlite";
2
2
  export declare function assertHeartSchemaVersion(database: DatabaseSync): void;
3
3
  export declare function assertLeashSchemaVersion(database: DatabaseSync): void;
4
+ export declare function heartSchemaVersion(database: DatabaseSync): number | undefined;
4
5
  export declare function heartSchemaIsCurrent(database: DatabaseSync): boolean;
5
- export declare const HEART_SCHEMA = "\n CREATE TABLE IF NOT EXISTS akuma_schema (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n version INTEGER NOT NULL CHECK (version = 29)\n ) STRICT;\n INSERT OR IGNORE INTO akuma_schema(singleton, version) VALUES (1, 29);\n CREATE TABLE IF NOT EXISTS soul (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n soul_json TEXT NOT NULL CHECK (json_valid(soul_json))\n ) STRICT;\n CREATE TABLE IF NOT EXISTS bodies (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n leash_taken_at TEXT NOT NULL,\n hung_diagnostic TEXT,\n hung_at TEXT,\n end TEXT CHECK (end IN ('exited', 'broke-off', 'put-down')),\n ended_at TEXT,\n CHECK ((hung_diagnostic IS NULL AND hung_at IS NULL)\n OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL))\n ) STRICT;\n CREATE TABLE IF NOT EXISTS sessions (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n provider TEXT NOT NULL,\n coordinate_json TEXT NOT NULL CHECK (json_valid(coordinate_json)),\n cwd TEXT NOT NULL,\n options_json TEXT NOT NULL CHECK (json_valid(options_json)),\n admitted_at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS turns (\n sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,\n body_sequence INTEGER NOT NULL REFERENCES bodies(sequence),\n started_at TEXT NOT NULL,\n initiator TEXT,\n end_sequence INTEGER UNIQUE REFERENCES timeline(sequence) ON DELETE SET NULL,\n outcome TEXT CHECK (outcome IN ('answered', 'failed', 'invalid-output')),\n history_id TEXT UNIQUE,\n session_json TEXT CHECK (session_json IS NULL OR json_valid(session_json)),\n answer TEXT,\n answer_json TEXT CHECK (answer_json IS NULL OR json_valid(answer_json)),\n schema_json TEXT CHECK (schema_json IS NULL OR json_valid(schema_json)),\n diagnostic TEXT,\n completed_at TEXT,\n CHECK (\n (outcome = 'answered' AND session_json IS NOT NULL AND answer IS NOT NULL AND diagnostic IS NULL\n AND ((schema_json IS NULL AND answer_json IS NULL) OR (schema_json IS NOT NULL AND answer_json IS NOT NULL)))\n OR (outcome = 'failed' AND history_id IS NULL AND session_json IS NULL AND answer IS NULL\n AND answer_json IS NULL AND diagnostic IS NOT NULL)\n OR (outcome = 'invalid-output' AND answer IS NOT NULL AND answer_json IS NULL\n AND diagnostic IS NOT NULL AND schema_json IS NOT NULL)\n OR (outcome IS NULL AND end_sequence IS NULL AND history_id IS NULL AND session_json IS NULL\n AND answer IS NULL AND answer_json IS NULL AND diagnostic IS NULL AND completed_at IS NULL)\n )\n ) STRICT;\n CREATE TABLE IF NOT EXISTS timeline (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL CHECK (kind IN ('turn-start', 'call', 'activity', 'tell', 'turn-end'))\n ) STRICT;\n CREATE TABLE IF NOT EXISTS activity_retention (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n checked_sequence INTEGER NOT NULL\n ) STRICT;\n INSERT OR IGNORE INTO activity_retention(singleton, checked_sequence) VALUES (1, 0);\n CREATE TABLE IF NOT EXISTS calls (\n sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,\n turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,\n body TEXT NOT NULL,\n at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS activity (\n sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,\n turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,\n event_json TEXT NOT NULL CHECK (json_valid(event_json)),\n at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tells (\n id TEXT PRIMARY KEY,\n sequence INTEGER NOT NULL UNIQUE REFERENCES timeline(sequence) ON DELETE CASCADE,\n body TEXT NOT NULL,\n initiator TEXT,\n schema_json TEXT CHECK (schema_json IS NULL OR json_valid(schema_json)),\n recorded_at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tell_bindings (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n tell_id TEXT NOT NULL REFERENCES tells(id) ON DELETE CASCADE,\n turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,\n bound_at TEXT NOT NULL,\n UNIQUE (tell_id, turn_sequence)\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tell_deliveries (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n tell_id TEXT NOT NULL REFERENCES tells(id) ON DELETE CASCADE,\n route TEXT NOT NULL CHECK (route IN ('launch', 'live')),\n turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,\n fence TEXT NOT NULL,\n receipt TEXT CHECK (receipt IN ('unavailable', 'required')),\n delivered_at TEXT NOT NULL,\n CHECK ((route = 'launch' AND receipt IS NULL) OR (route = 'live' AND receipt IS NOT NULL)),\n UNIQUE (tell_id, turn_sequence, fence)\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tell_receipts (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n evidence TEXT NOT NULL CHECK (evidence IN ('exact', 'fence')),\n tell_id TEXT REFERENCES tells(id) ON DELETE CASCADE,\n turn_sequence INTEGER REFERENCES turns(sequence) ON DELETE CASCADE,\n fence TEXT,\n kind TEXT NOT NULL,\n received_at TEXT NOT NULL,\n CHECK (\n (evidence = 'exact' AND tell_id IS NOT NULL AND turn_sequence IS NULL AND fence IS NULL)\n OR (evidence = 'fence' AND tell_id IS NULL AND turn_sequence IS NOT NULL AND fence IS NOT NULL)\n )\n ) STRICT;\n CREATE UNIQUE INDEX IF NOT EXISTS tell_receipts_exact\n ON tell_receipts(tell_id, kind);\n CREATE UNIQUE INDEX IF NOT EXISTS tell_receipts_fence\n ON tell_receipts(turn_sequence, fence, kind);\n CREATE TABLE IF NOT EXISTS tell_dispositions (\n body_sequence INTEGER PRIMARY KEY REFERENCES bodies(sequence),\n decided_at TEXT NOT NULL,\n resolved_at TEXT\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tell_disposition_members (\n body_sequence INTEGER NOT NULL REFERENCES tell_dispositions(body_sequence) ON DELETE CASCADE,\n tell_id TEXT NOT NULL REFERENCES tells(id),\n PRIMARY KEY (body_sequence, tell_id)\n ) STRICT;\n CREATE INDEX IF NOT EXISTS calls_by_turn ON calls(turn_sequence);\n CREATE INDEX IF NOT EXISTS activity_by_turn ON activity(turn_sequence);\n CREATE INDEX IF NOT EXISTS tell_bindings_by_turn ON tell_bindings(turn_sequence);\n CREATE INDEX IF NOT EXISTS tell_deliveries_by_turn ON tell_deliveries(turn_sequence);\n CREATE INDEX IF NOT EXISTS tell_disposition_members_by_tell ON tell_disposition_members(tell_id);\n CREATE TABLE IF NOT EXISTS requests (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n id TEXT NOT NULL UNIQUE,\n requester TEXT NOT NULL,\n action TEXT NOT NULL,\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n admitted_at TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('admitted', 'reserved', 'begun', 'served', 'refused', 'voided', 'unproven')),\n child TEXT,\n service_json TEXT CHECK (service_json IS NULL OR json_valid(service_json)),\n diagnostic TEXT,\n evidence TEXT,\n CHECK (\n (state = 'admitted' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'reserved' AND child IS NOT NULL AND service_json IS NULL\n AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'begun' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'served'\n AND ((child IS NOT NULL AND service_json IS NULL)\n OR (child IS NULL AND service_json IS NOT NULL))\n AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'refused' AND child IS NULL AND service_json IS NULL AND diagnostic IS NOT NULL AND evidence IS NULL)\n OR (state = 'voided' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NOT NULL)\n OR (state = 'unproven' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NOT NULL)\n )\n ) STRICT;\n CREATE TABLE IF NOT EXISTS control (\n kind TEXT PRIMARY KEY CHECK (kind IN ('stop', 'pause')),\n value_json TEXT NOT NULL CHECK (json_valid(value_json)),\n at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS kills (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n body_sequence INTEGER NOT NULL UNIQUE REFERENCES bodies(sequence),\n evidence TEXT NOT NULL CHECK (evidence = 'killed'),\n at TEXT NOT NULL\n ) STRICT;\n";
6
+ /** Called only under the open boundary's write transaction, with foreign keys disabled. */
7
+ export declare function upgradeHeartSchema(database: DatabaseSync): void;
8
+ export declare const HEART_SCHEMA: string;
6
9
  export declare const LEASH_SCHEMA = "\n PRAGMA journal_mode=DELETE;\n CREATE TABLE IF NOT EXISTS leash_schema (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n version INTEGER NOT NULL CHECK (version = 4)\n ) STRICT;\n INSERT OR IGNORE INTO leash_schema(singleton, version) VALUES (1, 4);\n CREATE TABLE IF NOT EXISTS seal (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n evidence TEXT NOT NULL,\n at TEXT NOT NULL\n ) STRICT;\n";
@@ -1,4 +1,4 @@
1
- const HEART_SCHEMA_VERSION = 29;
1
+ const HEART_SCHEMA_VERSION = 31;
2
2
  const LEASH_SCHEMA_VERSION = 4;
3
3
  function assertSchemaVersion(database, table, expected) {
4
4
  const row = database.prepare(`SELECT version FROM ${table} WHERE singleton = 1`).get();
@@ -11,50 +11,33 @@ export function assertHeartSchemaVersion(database) {
11
11
  export function assertLeashSchemaVersion(database) {
12
12
  assertSchemaVersion(database, "leash_schema", LEASH_SCHEMA_VERSION);
13
13
  }
14
+ export function heartSchemaVersion(database) {
15
+ return database.prepare("SELECT version FROM akuma_schema WHERE singleton = 1").get()?.version;
16
+ }
14
17
  export function heartSchemaIsCurrent(database) {
15
18
  try {
16
- const row = database.prepare(`SELECT version FROM akuma_schema WHERE singleton = 1`).get();
17
- return row?.version === HEART_SCHEMA_VERSION;
19
+ return heartSchemaVersion(database) === HEART_SCHEMA_VERSION;
18
20
  }
19
21
  catch {
20
22
  return false;
21
23
  }
22
24
  }
23
- export const HEART_SCHEMA = `
25
+ const VERSION_SCHEMA = `
24
26
  CREATE TABLE IF NOT EXISTS akuma_schema (
25
27
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
26
28
  version INTEGER NOT NULL CHECK (version = ${HEART_SCHEMA_VERSION})
27
29
  ) STRICT;
28
30
  INSERT OR IGNORE INTO akuma_schema(singleton, version) VALUES (1, ${HEART_SCHEMA_VERSION});
29
- CREATE TABLE IF NOT EXISTS soul (
30
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
31
- soul_json TEXT NOT NULL CHECK (json_valid(soul_json))
32
- ) STRICT;
33
- CREATE TABLE IF NOT EXISTS bodies (
34
- sequence INTEGER PRIMARY KEY AUTOINCREMENT,
35
- leash_taken_at TEXT NOT NULL,
36
- hung_diagnostic TEXT,
37
- hung_at TEXT,
38
- end TEXT CHECK (end IN ('exited', 'broke-off', 'put-down')),
39
- ended_at TEXT,
40
- CHECK ((hung_diagnostic IS NULL AND hung_at IS NULL)
41
- OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL))
42
- ) STRICT;
43
- CREATE TABLE IF NOT EXISTS sessions (
44
- sequence INTEGER PRIMARY KEY AUTOINCREMENT,
45
- provider TEXT NOT NULL,
46
- coordinate_json TEXT NOT NULL CHECK (json_valid(coordinate_json)),
47
- cwd TEXT NOT NULL,
48
- options_json TEXT NOT NULL CHECK (json_valid(options_json)),
49
- admitted_at TEXT NOT NULL
50
- ) STRICT;
51
- CREATE TABLE IF NOT EXISTS turns (
31
+ `;
32
+ function turnSchema(table) {
33
+ return `
34
+ CREATE TABLE ${table === "turns" ? "IF NOT EXISTS " : ""}${table} (
52
35
  sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,
53
36
  body_sequence INTEGER NOT NULL REFERENCES bodies(sequence),
54
37
  started_at TEXT NOT NULL,
55
38
  initiator TEXT,
56
39
  end_sequence INTEGER UNIQUE REFERENCES timeline(sequence) ON DELETE SET NULL,
57
- outcome TEXT CHECK (outcome IN ('answered', 'failed', 'invalid-output')),
40
+ outcome TEXT CHECK (outcome IN ('answered', 'failed', 'invalid-output', 'canceled')),
58
41
  history_id TEXT UNIQUE,
59
42
  session_json TEXT CHECK (session_json IS NULL OR json_valid(session_json)),
60
43
  answer TEXT,
@@ -69,14 +52,88 @@ export const HEART_SCHEMA = `
69
52
  AND answer_json IS NULL AND diagnostic IS NOT NULL)
70
53
  OR (outcome = 'invalid-output' AND answer IS NOT NULL AND answer_json IS NULL
71
54
  AND diagnostic IS NOT NULL AND schema_json IS NOT NULL)
55
+ OR (outcome = 'canceled' AND history_id IS NULL AND session_json IS NULL AND answer IS NULL
56
+ AND answer_json IS NULL AND diagnostic IS NULL)
72
57
  OR (outcome IS NULL AND end_sequence IS NULL AND history_id IS NULL AND session_json IS NULL
73
58
  AND answer IS NULL AND answer_json IS NULL AND diagnostic IS NULL AND completed_at IS NULL)
74
59
  )
60
+ ) STRICT;`;
61
+ }
62
+ /** Called only under the open boundary's write transaction, with foreign keys disabled. */
63
+ export function upgradeHeartSchema(database) {
64
+ const version = heartSchemaVersion(database);
65
+ // Another opener may have completed the fixed conversion while this one waited.
66
+ if (version !== 29 && version !== 30) {
67
+ assertHeartSchemaVersion(database);
68
+ return;
69
+ }
70
+ if (version === 29) {
71
+ database.exec(turnSchema("turns_upgrade"));
72
+ database.exec(`
73
+ INSERT INTO turns_upgrade (sequence, body_sequence, started_at, initiator, end_sequence,
74
+ outcome, history_id, session_json, answer, answer_json, schema_json, diagnostic, completed_at)
75
+ SELECT sequence, body_sequence, started_at, initiator, end_sequence,
76
+ outcome, history_id, session_json, answer, answer_json, schema_json, diagnostic, completed_at FROM turns;
77
+ DROP TABLE turns;
78
+ ALTER TABLE turns_upgrade RENAME TO turns;
79
+ `);
80
+ }
81
+ const highWater = database.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'timeline'").get();
82
+ database.exec(timelineSchema("timeline_upgrade"));
83
+ database.exec(`
84
+ INSERT INTO timeline_upgrade SELECT sequence, kind FROM timeline;
85
+ DROP TABLE timeline;
86
+ ALTER TABLE timeline_upgrade RENAME TO timeline;
87
+ ${REPORT_SCHEMA}
88
+ DROP TABLE akuma_schema;
89
+ ${VERSION_SCHEMA}
90
+ `);
91
+ if (highWater !== undefined)
92
+ database.prepare("UPDATE sqlite_sequence SET seq = MAX(seq, ?) WHERE name = 'timeline'").run(highWater.seq);
93
+ if (database.prepare("PRAGMA foreign_key_check").get() !== undefined)
94
+ throw new Error("Akuma Heart upgrade found broken foreign key relationships");
95
+ }
96
+ function timelineSchema(table) {
97
+ return `CREATE TABLE ${table === "timeline" ? "IF NOT EXISTS " : ""}${table} (
98
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ kind TEXT NOT NULL CHECK (kind IN ('turn-start', 'call', 'activity', 'tell', 'turn-end', 'report'))
100
+ ) STRICT;`;
101
+ }
102
+ const REPORT_SCHEMA = `CREATE TABLE IF NOT EXISTS reports (
103
+ sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,
104
+ body_sequence INTEGER NOT NULL REFERENCES bodies(sequence),
105
+ turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,
106
+ text TEXT NOT NULL,
107
+ at TEXT NOT NULL
108
+ ) STRICT;
109
+ CREATE INDEX IF NOT EXISTS reports_by_turn ON reports(turn_sequence);`;
110
+ export const HEART_SCHEMA = `
111
+ ${VERSION_SCHEMA}
112
+ CREATE TABLE IF NOT EXISTS soul (
113
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
114
+ soul_json TEXT NOT NULL CHECK (json_valid(soul_json))
75
115
  ) STRICT;
76
- CREATE TABLE IF NOT EXISTS timeline (
116
+ CREATE TABLE IF NOT EXISTS bodies (
77
117
  sequence INTEGER PRIMARY KEY AUTOINCREMENT,
78
- kind TEXT NOT NULL CHECK (kind IN ('turn-start', 'call', 'activity', 'tell', 'turn-end'))
118
+ leash_taken_at TEXT NOT NULL,
119
+ hung_diagnostic TEXT,
120
+ hung_at TEXT,
121
+ end TEXT CHECK (end IN ('exited', 'broke-off', 'put-down')),
122
+ ended_at TEXT,
123
+ CHECK ((hung_diagnostic IS NULL AND hung_at IS NULL)
124
+ OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL))
125
+ ) STRICT;
126
+ CREATE TABLE IF NOT EXISTS sessions (
127
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
128
+ provider TEXT NOT NULL,
129
+ coordinate_json TEXT NOT NULL CHECK (json_valid(coordinate_json)),
130
+ cwd TEXT NOT NULL,
131
+ options_json TEXT NOT NULL CHECK (json_valid(options_json)),
132
+ admitted_at TEXT NOT NULL
79
133
  ) STRICT;
134
+ ${turnSchema("turns")}
135
+ ${timelineSchema("timeline")}
136
+ ${REPORT_SCHEMA}
80
137
  CREATE TABLE IF NOT EXISTS activity_retention (
81
138
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
82
139
  checked_sequence INTEGER NOT NULL
@@ -35,6 +35,9 @@ export function decodeSoulSeed(value) {
35
35
  const provider = decodeProviderRecipe(soul.provider);
36
36
  const options = decodeProviderOptions(soul.options);
37
37
  const caller = decodeAkumaCaller(soul.caller);
38
+ if (soul.bare !== undefined && typeof soul.bare !== "boolean") {
39
+ throw new Error("Akuma soul bare must be a boolean");
40
+ }
38
41
  return {
39
42
  id,
40
43
  archetype,
@@ -45,6 +48,7 @@ export function decodeSoulSeed(value) {
45
48
  origin: akumaOriginSchema.parse(soul.origin),
46
49
  ...(caller === undefined ? {} : { caller }),
47
50
  allowed: historicalAllowedActions(soul.allowed),
51
+ ...(soul.bare === undefined ? {} : { bare: soul.bare }),
48
52
  };
49
53
  }
50
54
  function deepFreeze(value) {
@@ -47,6 +47,7 @@ export declare class HeldAkumaLeash {
47
47
  completeInterruptBarrier(paths: AkumaPaths, expected?: PauseFact): Promise<InterruptCompletion>;
48
48
  recordBody(paths: AkumaPaths, input: Readonly<{
49
49
  leashTakenAt: string;
50
+ handoffFromBodySequence?: number;
50
51
  }>): Promise<BodyFact>;
51
52
  recordBodyHung(paths: AkumaPaths, input: Readonly<{
52
53
  sequence: number;
@@ -3,9 +3,9 @@ import { lstat } from "node:fs/promises";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { setTimeout } from "node:timers/promises";
5
5
  import { BodyControlledError } from "./facts.js";
6
- import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion, heartSchemaIsCurrent, } from "./schema.js";
7
- import { deleteStopControl, completeInterruptBarrier as judgeInterruptBarrier, insertBodyFact, insertKillFact, insertSealFact, insertSessionFact, killFactForBody, latestBodyFact, latestKillFact, markBodyHung, sealExists, sealFact, stopFact, } from "./rows.js";
8
- import { insertTellFact } from "./tells.js";
6
+ import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion, heartSchemaIsCurrent, heartSchemaVersion, upgradeHeartSchema, } from "./schema.js";
7
+ import { deleteStopControl, completeInterruptBarrier as judgeInterruptBarrier, insertBodyFact, insertKillFact, insertSealFact, insertSessionFact, killFactForBody, hasBodyHandoffControl, latestBodyFact, latestKillFact, markBodyHung, sealExists, sealFact, stopFact, } from "./rows.js";
8
+ import { insertTellFact, resolveTellDispositionSnapshot, tellDispositionResolved } from "./tells.js";
9
9
  import { pruneActivityFacts } from "./timeline.js";
10
10
  import { insertSoulFact, soulFact } from "./soul.js";
11
11
  const ACTIVITY_LIMIT = 5_000;
@@ -46,16 +46,43 @@ async function openExistingDatabase(path, timeout, mode = "rw") {
46
46
  throw error;
47
47
  }
48
48
  }
49
- async function openHeart(path, mode) {
49
+ async function openHeart(path, mode, signal) {
50
50
  const database = await openExistingDatabase(path, 0, mode);
51
51
  try {
52
52
  database.exec("PRAGMA foreign_keys=ON; PRAGMA busy_timeout=0");
53
- return database;
53
+ const version = await acquire(() => {
54
+ const version = heartSchemaVersion(database);
55
+ if (version !== 29 && version !== 30)
56
+ assertHeartSchemaVersion(database);
57
+ return version;
58
+ }, signal);
59
+ if (version !== 29 && version !== 30)
60
+ return database;
54
61
  }
55
62
  catch (error) {
56
63
  database.close();
57
64
  throw error;
58
65
  }
66
+ database.close();
67
+ // Current-format reads never acquire a writer. Only the known predecessors
68
+ // need one, and no read transaction survives across the conversion.
69
+ const writer = await openExistingDatabase(path, 0);
70
+ try {
71
+ // Dropping turns with FK enforcement enabled would cascade into its children.
72
+ // SQLite requires this switch outside the transaction; check before commit.
73
+ writer.exec("PRAGMA foreign_keys=OFF; PRAGMA busy_timeout=0");
74
+ await beginTransaction(writer, "write", signal, heartSchemaVersion);
75
+ finishTransaction(writer, () => upgradeHeartSchema(writer));
76
+ }
77
+ finally {
78
+ try {
79
+ writer.exec("PRAGMA foreign_keys=ON");
80
+ }
81
+ finally {
82
+ writer.close();
83
+ }
84
+ }
85
+ return await openHeart(path, mode, signal);
59
86
  }
60
87
  export async function classifyHeartSchema(paths) {
61
88
  let database;
@@ -74,20 +101,15 @@ export async function classifyHeartSchema(paths) {
74
101
  database.close();
75
102
  }
76
103
  }
77
- async function beginTransaction(database, mode, signal) {
104
+ async function acquire(operation, signal) {
78
105
  const deadline = performance.now() + TRANSACTION_ACQUISITION_MS;
79
106
  let delay = 10;
80
107
  for (;;) {
81
108
  signal?.throwIfAborted();
82
109
  try {
83
- database.exec(mode === "write" ? "BEGIN IMMEDIATE" : "BEGIN DEFERRED");
84
- // DEFERRED acquires its snapshot on the first read, not on BEGIN. WAL
85
- // recovery and last-connection cleanup can make this read busy too.
86
- assertHeartSchemaVersion(database);
87
- return;
110
+ return operation();
88
111
  }
89
112
  catch (error) {
90
- rollbackTransaction(database);
91
113
  // Only failed admission can be retried: no caller statement has run.
92
114
  const remaining = deadline - performance.now();
93
115
  if (!isBusy(error) || remaining <= 0)
@@ -102,9 +124,22 @@ async function beginTransaction(database, mode, signal) {
102
124
  }
103
125
  }
104
126
  }
127
+ async function beginTransaction(database, mode, signal, validate = assertHeartSchemaVersion) {
128
+ await acquire(() => {
129
+ try {
130
+ database.exec(mode === "write" ? "BEGIN IMMEDIATE" : "BEGIN DEFERRED");
131
+ // DEFERRED acquires its snapshot on the first read, not on BEGIN.
132
+ validate(database);
133
+ }
134
+ catch (error) {
135
+ rollbackTransaction(database);
136
+ throw error;
137
+ }
138
+ }, signal);
139
+ }
105
140
  export async function withReadOnlyHeart(paths, body, signal) {
106
141
  signal?.throwIfAborted();
107
- const database = await openHeart(paths.heart, "ro");
142
+ const database = await openHeart(paths.heart, "ro", signal);
108
143
  try {
109
144
  await beginTransaction(database, "read", signal);
110
145
  return finishTransaction(database, body);
@@ -151,7 +186,7 @@ export function createHeartReader(paths) {
151
186
  if (identity?.dev !== current.dev || identity?.ino !== current.ino)
152
187
  release();
153
188
  if (database === undefined) {
154
- database = await openHeart(paths.heart, "ro");
189
+ database = await openHeart(paths.heart, "ro", signal);
155
190
  identity = current;
156
191
  opened += 1;
157
192
  }
@@ -170,7 +205,7 @@ export function createHeartReader(paths) {
170
205
  }
171
206
  export async function withHeartTransaction(paths, body, signal) {
172
207
  signal?.throwIfAborted();
173
- const database = await openHeart(paths.heart, "rw");
208
+ const database = await openHeart(paths.heart, "rw", signal);
174
209
  try {
175
210
  await beginTransaction(database, "write", signal);
176
211
  return finishTransaction(database, body);
@@ -282,6 +317,12 @@ export class HeldAkumaLeash {
282
317
  if (this.closed || this.bodySequence !== undefined)
283
318
  throw new Error("Akuma leash cannot start another Body");
284
319
  const sequence = await withHeartTransaction(paths, (heart) => {
320
+ const predecessor = input.handoffFromBodySequence;
321
+ if (predecessor !== undefined &&
322
+ (latestBodyFact(heart)?.sequence !== predecessor ||
323
+ hasBodyHandoffControl(heart, predecessor) ||
324
+ tellDispositionResolved(heart, predecessor) !== false))
325
+ throw new BodyControlledError();
285
326
  const completion = judgeInterruptBarrier(heart);
286
327
  if (completion.kind === "unavailable")
287
328
  throw new BodyControlledError();
@@ -318,6 +359,9 @@ export class HeldAkumaLeash {
318
359
  const result = latestKillFact(heart)?.bodySequence === target.bodySequence
319
360
  ? "already-killed"
320
361
  : (insertKillFact(heart, target.bodySequence, target.requestedAt), "recorded");
362
+ const released = tellDispositionResolved(heart, target.bodySequence) === false;
363
+ resolveTellDispositionSnapshot(heart, target.bodySequence, target.requestedAt);
364
+ pruneActivityFacts(heart, ACTIVITY_LIMIT, released);
321
365
  deleteStopControl(heart);
322
366
  return { target, result };
323
367
  });
@@ -1,8 +1,8 @@
1
1
  import type { DatabaseSync } from "node:sqlite";
2
- import type { CallFact, TellFact, TurnEndFact, TurnStartFact } from "./facts.js";
3
- import { type ActivityFact, type ActivityRow, type CallRow, type TurnRow } from "./rows.js";
2
+ import type { CallFact, ReportFact, TellFact, TurnEndFact, TurnStartFact } from "./facts.js";
3
+ import { type ActivityFact, type ReportRow, type ActivityRow, type CallRow, type TurnRow } from "./rows.js";
4
4
  import { tellBatchAtSequences } from "./tells.js";
5
- export type TimelineFact = TurnStartFact | CallFact | ActivityFact | TellFact | TurnEndFact;
5
+ export type TimelineFact = TurnStartFact | CallFact | ReportFact | ActivityFact | TellFact | TurnEndFact;
6
6
  export type ActivityFactSlice = Readonly<{
7
7
  rows: readonly TimelineFact[];
8
8
  lowestRetained: number | null;
@@ -12,12 +12,13 @@ export declare function lastActivityAt(database: DatabaseSync): string | null;
12
12
  export declare function pruneActivityFacts(database: DatabaseSync, limit: number, protectionReleased?: boolean): void;
13
13
  type TimelineRow = Readonly<{
14
14
  sequence: number;
15
- kind: "turn-start" | "call" | "activity" | "tell" | "turn-end";
15
+ kind: "turn-start" | "call" | "report" | "activity" | "tell" | "turn-end";
16
16
  }>;
17
17
  type TimelineBatch = Readonly<{
18
18
  rows: readonly TimelineRow[];
19
19
  turns: readonly TurnRow[];
20
20
  calls: readonly CallRow[];
21
+ reports: readonly ReportRow[];
21
22
  activity: readonly ActivityRow[];
22
23
  tells: ReturnType<typeof tellBatchAtSequences>;
23
24
  }>;
@@ -31,6 +32,7 @@ export declare function readStatusBatch(database: DatabaseSync, input: StatusFac
31
32
  rows: readonly TimelineRow[];
32
33
  turns: readonly TurnRow[];
33
34
  calls: readonly CallRow[];
35
+ reports: readonly ReportRow[];
34
36
  activity: readonly ActivityRow[];
35
37
  tells: ReturnType<typeof tellBatchAtSequences>;
36
38
  frontier: number | null;
@@ -1,4 +1,4 @@
1
- import { decodeActivityRow, decodeCallRow, decodeTurnRow, } from "./rows.js";
1
+ import { decodeReportRow, decodeActivityRow, decodeCallRow, decodeTurnRow, } from "./rows.js";
2
2
  import { decodeTellBatch, tellBatchAtSequences, tellStateSql, pendingTellProtectionSql, pendingTellSequencesSql, } from "./tells.js";
3
3
  export function lastActivityAt(database) {
4
4
  const row = database
@@ -6,6 +6,7 @@ export function lastActivityAt(database) {
6
6
  WHEN 'turn-start' THEN started_turn.started_at
7
7
  WHEN 'call' THEN calls.at
8
8
  WHEN 'activity' THEN activity.at
9
+ WHEN 'report' THEN reports.at
9
10
  WHEN 'tell' THEN tells.recorded_at
10
11
  WHEN 'turn-end' THEN ended_turn.completed_at
11
12
  END AS at
@@ -13,6 +14,7 @@ export function lastActivityAt(database) {
13
14
  LEFT JOIN turns AS started_turn ON timeline.kind = 'turn-start' AND started_turn.sequence = timeline.sequence
14
15
  LEFT JOIN calls ON timeline.kind = 'call' AND calls.sequence = timeline.sequence
15
16
  LEFT JOIN activity ON timeline.kind = 'activity' AND activity.sequence = timeline.sequence
17
+ LEFT JOIN reports ON timeline.kind = 'report' AND reports.sequence = timeline.sequence
16
18
  LEFT JOIN tells ON timeline.kind = 'tell' AND tells.sequence = timeline.sequence
17
19
  LEFT JOIN turns AS ended_turn ON timeline.kind = 'turn-end' AND ended_turn.end_sequence = timeline.sequence
18
20
  ORDER BY timeline.sequence DESC LIMIT 1`)
@@ -41,6 +43,7 @@ export function pruneActivityFacts(database, limit, protectionReleased = false)
41
43
  SELECT sequence FROM turns WHERE end_sequence IS NULL
42
44
  UNION SELECT turn_sequence FROM calls WHERE sequence >= ?
43
45
  UNION SELECT turn_sequence FROM activity WHERE sequence >= ?
46
+ UNION SELECT turn_sequence FROM reports WHERE sequence >= ?
44
47
  UNION SELECT sequence FROM turns WHERE end_sequence >= ?
45
48
  UNION ${pendingTellProtectionSql}
46
49
  ), protected(sequence) AS (
@@ -50,7 +53,7 @@ export function pruneActivityFacts(database, limit, protectionReleased = false)
50
53
  UNION ${pendingTellSequencesSql}
51
54
  )
52
55
  DELETE FROM timeline WHERE sequence < ? AND sequence NOT IN protected`)
53
- .run(cutoff.sequence, cutoff.sequence, cutoff.sequence, cutoff.sequence);
56
+ .run(cutoff.sequence, cutoff.sequence, cutoff.sequence, cutoff.sequence, cutoff.sequence);
54
57
  }
55
58
  function readTimelineBatch(database, rows) {
56
59
  const sequences = (kind) => JSON.stringify(rows.filter((row) => row.kind === kind).map((row) => row.sequence));
@@ -68,8 +71,12 @@ function readTimelineBatch(database, rows) {
68
71
  .prepare(`SELECT sequence, turn_sequence, event_json, at FROM activity
69
72
  WHERE sequence IN (SELECT value FROM json_each(?)) ORDER BY sequence`)
70
73
  .all(sequences("activity"));
74
+ const reports = database
75
+ .prepare(`SELECT sequence, body_sequence, turn_sequence, text, at
76
+ FROM reports WHERE sequence IN (SELECT value FROM json_each(?)) ORDER BY sequence`)
77
+ .all(sequences("report"));
71
78
  const tells = tellBatchAtSequences(database, rows.filter((row) => row.kind === "tell").map((row) => row.sequence));
72
- return { rows, turns, calls, activity, tells };
79
+ return { rows, turns, calls, reports, activity, tells };
73
80
  }
74
81
  export function decodeTimelineBatch(batch) {
75
82
  const facts = new Map();
@@ -81,6 +88,8 @@ export function decodeTimelineBatch(batch) {
81
88
  }
82
89
  for (const row of batch.calls)
83
90
  facts.set(row.sequence, decodeCallRow(row));
91
+ for (const row of batch.reports)
92
+ facts.set(row.sequence, decodeReportRow(row));
84
93
  for (const row of batch.activity)
85
94
  facts.set(row.sequence, decodeActivityRow(row));
86
95
  for (const fact of decodeTellBatch(batch.tells))
@@ -100,6 +109,7 @@ export function readStatusBatch(database, input) {
100
109
  SELECT sequence FROM turns WHERE sequence = ?
101
110
  UNION SELECT end_sequence FROM turns WHERE sequence = ? AND end_sequence IS NOT NULL
102
111
  UNION SELECT sequence FROM calls WHERE turn_sequence = ?
112
+ UNION SELECT sequence FROM reports WHERE turn_sequence = ?
103
113
  UNION SELECT sequence FROM activity WHERE turn_sequence = ?
104
114
  UNION SELECT tells.sequence FROM tells
105
115
  JOIN tell_deliveries ON tell_deliveries.tell_id = tells.id
@@ -107,7 +117,7 @@ export function readStatusBatch(database, input) {
107
117
  UNION SELECT sequence FROM tells WHERE id = ?
108
118
  UNION SELECT sequence FROM tells WHERE ? AND ${tellStateSql} = 'pending'
109
119
  ) SELECT timeline.sequence, timeline.kind FROM timeline JOIN selected USING(sequence) ORDER BY sequence`)
110
- .all(frontier?.sequence ?? null, frontier?.sequence ?? null, frontier?.sequence ?? null, frontier?.sequence ?? null, frontier?.sequence ?? null, input.admittedTellId ?? null, input.aperture === "monitoring" || input.admittedTellId === undefined ? 1 : 0);
120
+ .all(frontier?.sequence ?? null, frontier?.sequence ?? null, frontier?.sequence ?? null, frontier?.sequence ?? null, frontier?.sequence ?? null, frontier?.sequence ?? null, input.admittedTellId ?? null, input.aperture === "monitoring" || input.admittedTellId === undefined ? 1 : 0);
111
121
  return { frontier: frontier?.sequence ?? null, ...readTimelineBatch(database, rows) };
112
122
  }
113
123
  export function statusFacts(database, input) {
@@ -0,0 +1,5 @@
1
+ import type { ProviderAdapter } from "./provider.js";
2
+ import { type BodyRuntime } from "./body.js";
3
+ import type { BodyLaunch, LiveBodyLaunch } from "./body-launch.js";
4
+ /** One owned Body in this Node runtime; preparation starts only after custody is returned. */
5
+ export declare function launchInProcessBody(launch: BodyLaunch, prepare: (signal: AbortSignal) => Promise<Pick<BodyRuntime, "world" | "externalCommands" | "spawn" | "requestSpawn" | "executionAssociation">>, adapter?: ProviderAdapter, scopeSignal?: AbortSignal): Promise<LiveBodyLaunch>;
@@ -0,0 +1,57 @@
1
+ import { driveAkumaBody } from "./body.js";
2
+ import { abortable } from "./abort.js";
3
+ /** One owned Body in this Node runtime; preparation starts only after custody is returned. */
4
+ export async function launchInProcessBody(launch, prepare, adapter, scopeSignal) {
5
+ const controller = new AbortController();
6
+ const forced = new AbortController();
7
+ // Scope custody is independent of the publication holder's release below.
8
+ const closeScope = () => {
9
+ forced.abort();
10
+ controller.abort(scopeSignal?.reason);
11
+ };
12
+ scopeSignal?.addEventListener("abort", closeScope, { once: true });
13
+ if (scopeSignal?.aborted)
14
+ closeScope();
15
+ let released = false;
16
+ // A pending SDK/setup Promise alone does not keep Node alive. Release is not
17
+ // detachment: this Body retains the calling runtime until its drive finishes.
18
+ const lifetime = setInterval(() => { }, 1_073_741_824);
19
+ const completed = Promise.resolve()
20
+ .then(async () => {
21
+ controller.signal.throwIfAborted();
22
+ const runtime = await abortable(prepare(controller.signal), controller.signal);
23
+ controller.signal.throwIfAborted();
24
+ const result = await driveAkumaBody(launch, adapter, {
25
+ ...runtime,
26
+ signal: controller.signal,
27
+ forceSignal: forced.signal,
28
+ });
29
+ if (result === "hung")
30
+ throw new Error("in-process Body ended with hung provider custody; native work may still be running");
31
+ return { diagnostic: "in-process Body completed", ...(result === "held" ? { held: true } : {}) };
32
+ })
33
+ .catch((error) => {
34
+ if (controller.signal.aborted && error === controller.signal.reason)
35
+ return { diagnostic: "in-process Body launch canceled" };
36
+ throw error;
37
+ })
38
+ .finally(() => {
39
+ scopeSignal?.removeEventListener("abort", closeScope);
40
+ clearInterval(lifetime);
41
+ });
42
+ void completed.catch(() => undefined);
43
+ return {
44
+ completed,
45
+ async cancel(force = false) {
46
+ if (released)
47
+ return;
48
+ if (force)
49
+ forced.abort();
50
+ controller.abort(new Error("in-process Body launch canceled"));
51
+ await completed;
52
+ },
53
+ release() {
54
+ released = true;
55
+ },
56
+ };
57
+ }
@@ -4,7 +4,7 @@ export { AkumaArchetypeError, listArchetypeDefinitions } from "./archetype.js";
4
4
  export type { ArchetypeCatalog, ArchetypeCatalogInput, ArchetypeCatalogRow } from "./archetype.js";
5
5
  export { AkumaBusyError, AkumaDecodeError, AkumaNotBornError, AkumaProviderError } from "./akuma-errors.js";
6
6
  export { Akuma } from "./akuma-instance.js";
7
- export type { AkumaAskOptions, AkumaBirthInput, AkumaHistoryOptions, AkumaHostOptions, AkumaIdleOptions, AkumaIdleResult, AkumaSignalOptions, AkumaTellOptions, } from "./akuma-instance.js";
7
+ export type { AkumaAskOptions, AkumaBirthInput, AkumaHistoryOptions, AkumaHostOptions, AkumaIdleOptions, AkumaIdleResult, AkumaReportOptions, AkumaReportResult, AkumaSignalOptions, AkumaTellOptions, } from "./akuma-instance.js";
8
8
  export type { AkumaStreamSnapshot } from "./akuma-stream.js";
9
9
  export type { InterruptReceipt } from "./akuma-owner.js";
10
10
  export type { AkumaStatus } from "./akuma.js";