@astrosheep/keiyaku 4.1.1 → 4.1.3

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 (75) hide show
  1. package/build/src/akuma/akuma-errors.d.ts +6 -0
  2. package/build/src/akuma/akuma-errors.js +9 -0
  3. package/build/src/akuma/akuma-handle.d.ts +39 -0
  4. package/build/src/akuma/akuma-handle.js +263 -0
  5. package/build/src/akuma/akuma-observe.d.ts +19 -0
  6. package/build/src/akuma/akuma-observe.js +74 -0
  7. package/build/src/akuma/akuma-product-symbols.d.ts +1 -0
  8. package/build/src/akuma/akuma-product-symbols.js +1 -0
  9. package/build/src/akuma/akuma-product.d.ts +18 -0
  10. package/build/src/akuma/akuma-product.js +154 -0
  11. package/build/src/akuma/akuma.d.ts +23 -70
  12. package/build/src/akuma/akuma.js +17 -433
  13. package/build/src/akuma/body-supervisor.d.ts +26 -0
  14. package/build/src/akuma/body-supervisor.js +89 -0
  15. package/build/src/akuma/body.d.ts +1 -1
  16. package/build/src/akuma/body.js +7 -3
  17. package/build/src/akuma/heart/index.d.ts +1 -1
  18. package/build/src/akuma/heart/index.js +1 -1
  19. package/build/src/akuma/heart/schema.d.ts +1 -0
  20. package/build/src/akuma/heart/schema.js +9 -0
  21. package/build/src/akuma/heart/storage.d.ts +1 -0
  22. package/build/src/akuma/heart/storage.js +18 -1
  23. package/build/src/akuma/nuke.js +6 -2
  24. package/build/src/akuma/projection-read.d.ts +17 -0
  25. package/build/src/akuma/projection-read.js +135 -0
  26. package/build/src/akuma/projection.d.ts +1 -18
  27. package/build/src/akuma/projection.js +21 -155
  28. package/build/src/akuma/providers/claude/index.js +101 -89
  29. package/build/src/akuma/providers/codex-app-server/index.js +9 -6
  30. package/build/src/akuma/providers/opencode-sdk/index.js +22 -15
  31. package/build/src/akuma/providers/pi/index.js +45 -43
  32. package/build/src/akuma/publication.d.ts +2 -1
  33. package/build/src/akuma/publication.js +72 -11
  34. package/build/src/akuma/request-execution.d.ts +8 -0
  35. package/build/src/akuma/request-execution.js +98 -0
  36. package/build/src/akuma/request-serve.d.ts +14 -2
  37. package/build/src/akuma/request-serve.js +3 -103
  38. package/build/src/akuma/{body-turn.d.ts → turn-drive.d.ts} +4 -26
  39. package/build/src/akuma/{body-turn.js → turn-drive.js} +14 -100
  40. package/build/src/cli/commands/akuma.d.ts +0 -6
  41. package/build/src/cli/commands/akuma.js +1 -11
  42. package/build/src/cli/commands/contract-invoke.d.ts +25 -0
  43. package/build/src/cli/commands/contract-invoke.js +193 -0
  44. package/build/src/cli/commands/contract.d.ts +12 -0
  45. package/build/src/cli/commands/contract.js +229 -0
  46. package/build/src/cli/invoke.js +24 -184
  47. package/build/src/cli/parse.d.ts +1 -1
  48. package/build/src/cli/parse.js +8 -222
  49. package/build/src/cli/render/akuma-activity.d.ts +38 -0
  50. package/build/src/cli/render/akuma-activity.js +362 -0
  51. package/build/src/cli/render/akuma.d.ts +1 -1
  52. package/build/src/cli/render/akuma.js +14 -371
  53. package/build/src/cli/render/contract-history.d.ts +2 -0
  54. package/build/src/cli/render/contract-history.js +83 -0
  55. package/build/src/cli/render/contract.d.ts +1 -2
  56. package/build/src/cli/render/contract.js +1 -83
  57. package/build/src/git/read-observation.js +15 -10
  58. package/build/src/git/reconcile.d.ts +8 -1
  59. package/build/src/git/reconcile.js +13 -301
  60. package/build/src/git/scratch.d.ts +8 -5
  61. package/build/src/git/scratch.js +29 -2
  62. package/build/src/git/terminal-reconcile.d.ts +21 -0
  63. package/build/src/git/terminal-reconcile.js +278 -0
  64. package/build/src/kanshi/read.js +32 -27
  65. package/build/src/library/contract-bind.d.ts +26 -0
  66. package/build/src/library/contract-bind.js +106 -0
  67. package/build/src/library/contract-operations.d.ts +77 -0
  68. package/build/src/library/contract-operations.js +130 -0
  69. package/build/src/library/contract-types.d.ts +95 -0
  70. package/build/src/library/contract-types.js +1 -0
  71. package/build/src/library/contract.d.ts +11 -136
  72. package/build/src/library/contract.js +9 -222
  73. package/build/src/library/fleet.js +2 -1
  74. package/build/src/runtime/proc/windows-launch.exe +0 -0
  75. package/package.json +1 -1
@@ -0,0 +1,89 @@
1
+ import { abortableDelay } from "./abort.js";
2
+ import { heartExists, readHeart } from "./heart/index.js";
3
+ const BODY_CONTROL_OBSERVATION_MS = 100;
4
+ export const CONTROL_RESPONSE_MS = 1_000;
5
+ export class BodySupervisor {
6
+ paths;
7
+ bodySequence;
8
+ leash;
9
+ signal;
10
+ controller = new AbortController();
11
+ finished = new AbortController();
12
+ observer;
13
+ waiter;
14
+ observation;
15
+ stopping;
16
+ constructor(paths, bodySequence, leash, observation) {
17
+ this.paths = paths;
18
+ this.bodySequence = bodySequence;
19
+ this.leash = leash;
20
+ this.signal = this.controller.signal;
21
+ this.observation = observation;
22
+ this.observer = this.observe();
23
+ }
24
+ static async open(paths, bodySequence, leash) {
25
+ return new BodySupervisor(paths, bodySequence, leash, await readHeart(paths));
26
+ }
27
+ get reason() {
28
+ return this.stopping;
29
+ }
30
+ current() {
31
+ return this.observation;
32
+ }
33
+ async recordHung(diagnostic, at) {
34
+ await this.leash.recordBodyHung(this.paths, { sequence: this.bodySequence, diagnostic, at });
35
+ }
36
+ async refresh() {
37
+ this.publish(await readHeart(this.paths));
38
+ return this.observation;
39
+ }
40
+ next(after) {
41
+ if (this.observation !== after)
42
+ return Promise.resolve(this.observation);
43
+ return new Promise((resolve) => {
44
+ this.waiter = resolve;
45
+ });
46
+ }
47
+ cancel(reason) {
48
+ if (this.stopping !== undefined)
49
+ return;
50
+ this.stopping = reason;
51
+ this.controller.abort(new Error(reason === "control" ? "Akuma Body interrupted by durable control" : "Akuma Heart disappeared"));
52
+ this.waiter?.(this.observation);
53
+ this.waiter = undefined;
54
+ }
55
+ async close() {
56
+ this.finished.abort();
57
+ await this.observer;
58
+ this.waiter = undefined;
59
+ }
60
+ publish(snapshot) {
61
+ this.observation = snapshot;
62
+ this.waiter?.(snapshot);
63
+ this.waiter = undefined;
64
+ }
65
+ async observe() {
66
+ for (;;) {
67
+ const snapshot = this.observation;
68
+ if (!(await heartExists(this.paths)))
69
+ return this.cancel("heart-gone");
70
+ if (snapshot.stop?.bodySequence === this.bodySequence || snapshot.pause?.bodySequence === this.bodySequence) {
71
+ return this.cancel("control");
72
+ }
73
+ try {
74
+ await abortableDelay(BODY_CONTROL_OBSERVATION_MS, this.finished.signal);
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ try {
80
+ await this.refresh();
81
+ }
82
+ catch (error) {
83
+ if (await heartExists(this.paths))
84
+ throw error;
85
+ return this.cancel("heart-gone");
86
+ }
87
+ }
88
+ }
89
+ }
@@ -4,7 +4,7 @@ import type { ProviderAdapter } from "./provider.js";
4
4
  import { type UpstreamExecutionPort, type RequestChildLaunch } from "./request-serve.js";
5
5
  import { type OwnedProcess, type RunLogReference } from "../runtime/proc/run.js";
6
6
  export declare const LEASH_HELD_EXIT = 75;
7
- export { CONTROL_RESPONSE_MS } from "./body-turn.js";
7
+ export { CONTROL_RESPONSE_MS } from "./body-supervisor.js";
8
8
  export type BodyLaunch = Readonly<{
9
9
  paths: AkumaPaths;
10
10
  seed?: Omit<Soul, "createdAt">;
@@ -1,13 +1,14 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { abortableDelay } from "./abort.js";
3
- import { BodySupervisor, driveTurn, turnRecipe } from "./body-turn.js";
3
+ import { BodySupervisor } from "./body-supervisor.js";
4
+ import { driveTurn, turnRecipe } from "./turn-drive.js";
4
5
  import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, watchHeart, } from "./heart/index.js";
5
6
  import { resolveProviderExecution } from "./providers/index.js";
6
7
  import { clearBodyRequestTransport, settleBodyRequests, } from "./request-serve.js";
7
8
  import { handoffProcess, spawnDetachedProcess, } from "../runtime/proc/run.js";
8
9
  const LEASH_RETRY_MS = 100;
9
10
  export const LEASH_HELD_EXIT = 75;
10
- export { CONTROL_RESPONSE_MS } from "./body-turn.js";
11
+ export { CONTROL_RESPONSE_MS } from "./body-supervisor.js";
11
12
  function missing(error) {
12
13
  return isHeartAbsent(error) || error.code === "ENOENT";
13
14
  }
@@ -83,8 +84,11 @@ export async function bodyProcessInput(launch, bodyModuleUrl = import.meta.url)
83
84
  throw new Error("Akuma wake has no born soul");
84
85
  const source = bodyModuleUrl.endsWith(".ts");
85
86
  const entry = fileURLToPath(new URL(source ? "../akuma-body.ts" : "../akuma-body.js", bodyModuleUrl));
87
+ const argv = source
88
+ ? [process.execPath, "--import", import.meta.resolve("tsx"), entry, encoded]
89
+ : [process.execPath, entry, encoded];
86
90
  return {
87
- argv: source ? [process.execPath, "--import", "tsx", entry, encoded] : [process.execPath, entry, encoded],
91
+ argv,
88
92
  cwd: await launchCwd(launch),
89
93
  env: { ...process.env, KEIYAKU_ACTOR_ID: actorId },
90
94
  log: launch.paths.log,
@@ -3,7 +3,7 @@ import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, KillFact, SealFact, S
3
3
  export type { CallFact } from "./facts.js";
4
4
  import type { ActivityFact } from "./rows.js";
5
5
  import { type ActivityFactSlice } from "./timeline.js";
6
- export { HeartAbsentError, HeldAkumaLeash, initializeHeart, isHeartAbsent, probeLeash, watchHeart } from "./storage.js";
6
+ export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
7
7
  export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
8
8
  export { life, lifeAt } from "./facts.js";
9
9
  export type { AkumaLife, AkumaOrigin, BodyEnd, BodyFact, HeartSnapshot, ForkPoint, KillEvidence, KillFact, LeashProbe, PauseFact, RequestFact, RequestInput, RequestRecipe, UpstreamRequestService, SealFact, SessionFact, Soul, StopFact, TellFact, TellDelivery, TellDeliveryInput, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact, } from "./facts.js";
@@ -3,7 +3,7 @@ import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingT
3
3
  import { activityFactSlice, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
4
4
  import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart } from "./storage.js";
5
5
  import { soulFact } from "./soul.js";
6
- export { HeartAbsentError, HeldAkumaLeash, initializeHeart, isHeartAbsent, probeLeash, watchHeart } from "./storage.js";
6
+ export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
7
7
  export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
8
8
  export { life, lifeAt } from "./facts.js";
9
9
  const ACTIVITY_LIMIT = 5_000;
@@ -1,5 +1,6 @@
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 heartSchemaIsCurrent(database: DatabaseSync): boolean;
4
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 = 20)\n ) STRICT;\n INSERT OR IGNORE INTO akuma_schema(singleton, version) VALUES (1, 20);\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 end_sequence INTEGER UNIQUE REFERENCES timeline(sequence) ON DELETE SET NULL,\n outcome TEXT CHECK (outcome IN ('answered', 'failed')),\n history_id TEXT UNIQUE,\n session_json TEXT CHECK (session_json IS NULL OR json_valid(session_json)),\n answer TEXT,\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 OR (outcome = 'failed' AND history_id IS NULL AND session_json IS NULL AND answer IS NULL AND diagnostic IS NOT NULL)\n OR (outcome IS NULL AND end_sequence IS NULL AND history_id IS NULL AND session_json IS NULL AND answer 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 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 recorded_at TEXT NOT NULL\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) WHERE evidence = 'exact';\n CREATE UNIQUE INDEX IF NOT EXISTS tell_receipts_fence\n ON tell_receipts(turn_sequence, fence, kind) WHERE evidence = 'fence';\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 CHECK (\n action IN (\n 'akuma.call', 'akuma.wait', 'akuma.tell', 'akuma.kill',\n 'contract.deliver', 'contract.review',\n 'task.add', 'task.addDocument', 'task.compose', 'task.update', 'task.start',\n 'task.stop', 'task.hold', 'task.resume', 'task.done', 'task.drop'\n )\n ),\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', 'served', 'refused', 'voided')),\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 action = 'akuma.call'\n AND child IS NOT NULL AND service_json IS NULL\n AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'served'\n AND ((action = 'akuma.call' AND child IS NOT NULL AND service_json IS NULL)\n OR (action != 'akuma.call' AND 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 )\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";
5
6
  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";
@@ -11,6 +11,15 @@ export function assertHeartSchemaVersion(database) {
11
11
  export function assertLeashSchemaVersion(database) {
12
12
  assertSchemaVersion(database, "leash_schema", LEASH_SCHEMA_VERSION);
13
13
  }
14
+ export function heartSchemaIsCurrent(database) {
15
+ try {
16
+ const row = database.prepare(`SELECT version FROM akuma_schema WHERE singleton = 1`).get();
17
+ return row?.version === HEART_SCHEMA_VERSION;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
14
23
  export const HEART_SCHEMA = `
15
24
  CREATE TABLE IF NOT EXISTS akuma_schema (
16
25
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
@@ -8,6 +8,7 @@ export declare class HeartAbsentError extends Error {
8
8
  constructor(path: string, options?: ErrorOptions);
9
9
  }
10
10
  export declare function isHeartAbsent(error: unknown): error is HeartAbsentError;
11
+ export declare function classifyHeartSchema(paths: AkumaPaths): Promise<"current" | "unsupported">;
11
12
  export declare function withHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
12
13
  export declare function transaction<T>(database: DatabaseSync, body: () => T): T;
13
14
  export declare function readTransaction<T>(database: DatabaseSync, body: () => T): T;
@@ -3,7 +3,7 @@ import { watch as watchDirectory } from "node:fs";
3
3
  import { lstat } from "node:fs/promises";
4
4
  import { basename, dirname } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
- import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion } from "./schema.js";
6
+ import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion, heartSchemaIsCurrent, } from "./schema.js";
7
7
  import { deletePauseControl, deleteStopControl, insertBodyFact, insertKillFact, insertSealFact, insertSessionFact, killFactForBody, latestBodyFact, latestKillFact, markBodyHung, sealExists, sealFact, stopFact, } from "./rows.js";
8
8
  import { insertTellFact } from "./tells.js";
9
9
  import { pruneActivityFacts } from "./timeline.js";
@@ -115,6 +115,23 @@ async function openHeart(path, verify = true) {
115
115
  throw error;
116
116
  }
117
117
  }
118
+ export async function classifyHeartSchema(paths) {
119
+ let database;
120
+ try {
121
+ database = await openExistingDatabase(paths.heart);
122
+ }
123
+ catch (error) {
124
+ if (isHeartAbsent(error))
125
+ return "unsupported";
126
+ throw error;
127
+ }
128
+ try {
129
+ return heartSchemaIsCurrent(database) ? "current" : "unsupported";
130
+ }
131
+ finally {
132
+ database.close();
133
+ }
134
+ }
118
135
  export async function withHeart(paths, body) {
119
136
  const database = await openHeart(paths.heart);
120
137
  try {
@@ -2,7 +2,7 @@ import { lstat, readdir, rm, rmdir } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { nukeAliases } from "../alias/index.js";
4
4
  import { CONTROL_RESPONSE_MS } from "./body.js";
5
- import { HeldAkumaLeash, readHeart, readKill, requestStop } from "./heart/index.js";
5
+ import { HeldAkumaLeash, classifyHeartSchema, readHeart, readKill, requestStop } from "./heart/index.js";
6
6
  import { akuIdFromDirectoryName, akumaPaths, akumaRunRoot } from "./identity.js";
7
7
  const POLL_MS = 100;
8
8
  async function hasAkumaCustody(paths) {
@@ -201,8 +201,11 @@ async function nukeAkumaEntries(world) {
201
201
  export async function stopAkuma(world) {
202
202
  const entries = await nukeAkumaEntries(world);
203
203
  const held = [];
204
+ let prepared = false;
204
205
  try {
205
206
  for (const entry of entries) {
207
+ if ((await classifyHeartSchema(entry.paths)) === "unsupported")
208
+ continue;
206
209
  const snapshot = await readHeart(entry.paths);
207
210
  const leash = snapshot.soul !== null && snapshot.latestBody?.end === undefined
208
211
  ? await stopRunningAkuma(entry)
@@ -222,6 +225,7 @@ export async function stopAkuma(world) {
222
225
  throw error;
223
226
  }
224
227
  }
228
+ prepared = true;
225
229
  return async () => {
226
230
  try {
227
231
  await nukeAliases(world);
@@ -236,7 +240,7 @@ export async function stopAkuma(world) {
236
240
  };
237
241
  }
238
242
  finally {
239
- if (held.length !== entries.length) {
243
+ if (!prepared) {
240
244
  for (const leash of held.reverse())
241
245
  leash.release();
242
246
  }
@@ -0,0 +1,17 @@
1
+ import type { ActivitySnapshot, TurnLedger } from "./projection.js";
2
+ import type { HistoryCursor, HistoryPage } from "./projection.js";
3
+ export declare function ordinarySnapshotBudget(ordinaryBudget?: number): Readonly<{
4
+ tail: number;
5
+ voice: number;
6
+ }>;
7
+ export declare function selectSnapshot(ledger: TurnLedger, input: Readonly<{
8
+ aperture: "monitoring" | "receipt";
9
+ budget?: Readonly<{
10
+ tail: number;
11
+ voice?: number;
12
+ }>;
13
+ }>): Readonly<{
14
+ snapshot: ActivitySnapshot;
15
+ ordinaryCount: number;
16
+ }>;
17
+ export declare function selectHistory(ledger: TurnLedger, cursor: HistoryCursor): HistoryPage;
@@ -0,0 +1,135 @@
1
+ const DEFAULT_TAIL = 3;
2
+ const DEFAULT_VOICE = 3;
3
+ const REPORTED_CHANGE_LIMIT = 5;
4
+ const EMPTY_REPORTED = { reportedChanges: [], reportedChangesOmitted: 0 };
5
+ function assembleEntries(ledger, window, selected) {
6
+ const windowRows = new Set(window);
7
+ const result = [];
8
+ let hidden = 0;
9
+ const flushGap = () => {
10
+ if (hidden === 0)
11
+ return;
12
+ result.push({ kind: "gap", count: hidden });
13
+ hidden = 0;
14
+ };
15
+ for (const row of ledger.rows) {
16
+ if (windowRows.has(row) && !selected.has(row)) {
17
+ hidden += 1;
18
+ continue;
19
+ }
20
+ if (!selected.has(row))
21
+ continue;
22
+ flushGap();
23
+ if (row.kind !== "turn" && row.kind !== "outcome" && !(row.kind === "tool" && row.state === "unsettled")) {
24
+ result.push({ kind: "row", row: row });
25
+ }
26
+ }
27
+ flushGap();
28
+ return result;
29
+ }
30
+ const DEFAULT_ORDINARY = DEFAULT_TAIL + DEFAULT_VOICE;
31
+ export function ordinarySnapshotBudget(ordinaryBudget = DEFAULT_ORDINARY) {
32
+ const tail = Math.min(DEFAULT_TAIL, Math.max(0, ordinaryBudget));
33
+ return { tail, voice: Math.min(DEFAULT_VOICE, Math.max(0, ordinaryBudget) - tail) };
34
+ }
35
+ function isSuccessfulFileChange(row) {
36
+ return (row.kind === "tool" && typeof row.state === "object" && row.state.status === "ok" && row.call.kind === "fileChange");
37
+ }
38
+ function flattenReportedChanges(rows) {
39
+ return rows.flatMap((row) => {
40
+ if (!isSuccessfulFileChange(row))
41
+ return [];
42
+ return row.call.changes.map((change) => ({
43
+ sequence: row.sequence,
44
+ at: row.at,
45
+ op: change.op,
46
+ path: change.path,
47
+ ...(change.diffstat === undefined ? {} : { diffstat: change.diffstat }),
48
+ }));
49
+ });
50
+ }
51
+ function summarizeReportedChanges(rows) {
52
+ const eligible = flattenReportedChanges(rows);
53
+ return {
54
+ reportedChanges: eligible.slice(-REPORTED_CHANGE_LIMIT),
55
+ reportedChangesOmitted: Math.max(0, eligible.length - REPORTED_CHANGE_LIMIT),
56
+ };
57
+ }
58
+ function frontierReportedChanges(ledger) {
59
+ if (ledger.openTurn !== undefined)
60
+ return summarizeReportedChanges(ledger.openTurn.rows);
61
+ const closed = ledger.turns.findLast((turn) => turn.kind === "closed");
62
+ return closed === undefined ? EMPTY_REPORTED : summarizeReportedChanges(closed.rows);
63
+ }
64
+ function isPendingTell(row) {
65
+ return row.kind === "tell" && row.state === "pending";
66
+ }
67
+ function isToldTell(row) {
68
+ return row.kind === "tell" && row.state === "told";
69
+ }
70
+ function isActiveTool(row) {
71
+ return row.kind === "tool" && row.state === "active";
72
+ }
73
+ export function selectSnapshot(ledger, input) {
74
+ const budget = input.budget ?? { tail: DEFAULT_TAIL, voice: DEFAULT_VOICE };
75
+ if (ledger.turns.length === 0 && !ledger.rows.some((row) => row.kind === "tell"))
76
+ return { snapshot: { kind: "unborn", entries: [], omitted: 0, ...EMPTY_REPORTED }, ordinaryCount: 0 };
77
+ const pending = ledger.rows.filter(isPendingTell);
78
+ const latestTold = input.aperture === "monitoring" ? ledger.rows.findLast(isToldTell) : undefined;
79
+ const pins = latestTold === undefined ? pending : [...pending, latestTold];
80
+ const reported = frontierReportedChanges(ledger);
81
+ if (ledger.openTurn !== undefined) {
82
+ const window = ledger.openTurn.rows;
83
+ const pinSet = new Set([...window.filter(isActiveTool), ...pins]);
84
+ const ordinary = window.filter((row) => !pinSet.has(row));
85
+ const tailCount = Math.max(0, budget.tail);
86
+ const tail = tailCount === 0 ? [] : ordinary.slice(-tailCount);
87
+ const tailSet = new Set(tail);
88
+ const voiceCount = Math.max(0, budget.voice ?? DEFAULT_VOICE);
89
+ const voiceCandidates = ordinary.filter((row) => !tailSet.has(row) && (row.kind === "said" || row.kind === "thought"));
90
+ const voice = voiceCount === 0 ? [] : voiceCandidates.slice(-voiceCount);
91
+ const selected = new Set([...pinSet, ...tail, ...voice]);
92
+ const omitted = window.filter((row) => !selected.has(row)).length;
93
+ return {
94
+ snapshot: {
95
+ kind: "open",
96
+ turn: ledger.openTurn.turn,
97
+ entries: assembleEntries(ledger, window, selected),
98
+ omitted,
99
+ ...reported,
100
+ },
101
+ ordinaryCount: tail.length + voice.length,
102
+ };
103
+ }
104
+ return {
105
+ snapshot: {
106
+ kind: "idle",
107
+ ...(ledger.latestOutcome === undefined ? {} : { outcome: ledger.latestOutcome }),
108
+ entries: assembleEntries(ledger, [], new Set(pins)),
109
+ omitted: 0,
110
+ ...reported,
111
+ },
112
+ ordinaryCount: 0,
113
+ };
114
+ }
115
+ export function selectHistory(ledger, cursor) {
116
+ const eligible = ledger.rows.filter((row) => cursor.before !== undefined
117
+ ? row.sequence < cursor.before
118
+ : cursor.since !== undefined
119
+ ? row.sequence > cursor.since
120
+ : true);
121
+ const rows = cursor.since !== undefined ? eligible.slice(0, cursor.limit) : eligible.slice(-cursor.limit);
122
+ const omitted = Math.max(0, eligible.length - rows.length);
123
+ const historyLost = cursor.since !== undefined
124
+ ? ledger.rows[0] !== undefined && ledger.rows[0].sequence > cursor.since + 1
125
+ : ledger.retained.lowestRetained !== null && ledger.retained.lowestRetained > 1 && omitted === 0;
126
+ return {
127
+ rows,
128
+ omitted,
129
+ hasEarlier: cursor.since === undefined && eligible.length > rows.length,
130
+ hasLater: cursor.since !== undefined && eligible.length > rows.length,
131
+ historyLost,
132
+ lowestRetained: ledger.retained.lowestRetained,
133
+ highest: ledger.retained.highest,
134
+ };
135
+ }
@@ -172,21 +172,4 @@ export type TurnLedger = Readonly<{
172
172
  }>;
173
173
  /** Fold the retained fact timeline into Turn-owned rows without inventing provider facts. */
174
174
  export declare function projectTurns(facts: readonly TimelineFact[], retained?: RetainedWindow): TurnLedger;
175
- /** Split one nonnegative ordinary allowance into newest tail first, then pre-tail voice. */
176
- export declare function ordinarySnapshotBudget(ordinaryBudget?: number): Readonly<{
177
- tail: number;
178
- voice: number;
179
- }>;
180
- /** Select one current Turn, one latest outcome, or no focus; pending tells stay actionable. */
181
- export declare function selectSnapshot(ledger: TurnLedger, input: Readonly<{
182
- aperture: "monitoring" | "receipt";
183
- budget?: Readonly<{
184
- tail: number;
185
- voice?: number;
186
- }>;
187
- }>): Readonly<{
188
- snapshot: ActivitySnapshot;
189
- ordinaryCount: number;
190
- }>;
191
- export declare function selectHistory(ledger: TurnLedger, cursor: HistoryCursor): HistoryPage;
192
- export {};
175
+ export { ordinarySnapshotBudget, selectHistory, selectSnapshot } from "./projection-read.js";
@@ -1,8 +1,4 @@
1
1
  import { decodeAgentEvent } from "./provider.js";
2
- const DEFAULT_TAIL = 3;
3
- const DEFAULT_VOICE = 3;
4
- const REPORTED_CHANGE_LIMIT = 5;
5
- const EMPTY_REPORTED = { reportedChanges: [], reportedChangesOmitted: 0 };
6
2
  function duration(startedAt, completedAt) {
7
3
  const start = Date.parse(startedAt);
8
4
  const end = Date.parse(completedAt);
@@ -124,6 +120,25 @@ function finishTurn(turn) {
124
120
  }
125
121
  return { kind: "open", turn: turn.turn, rows: rows };
126
122
  }
123
+ function finishLedger(state, retained) {
124
+ const turns = state.turns.map(finishTurn);
125
+ const rows = [
126
+ ...turns.flatMap((turn) => [turn.turn, ...turn.rows, ...(turn.kind === "closed" ? [turn.outcome] : [])]),
127
+ ...state.tells,
128
+ ...state.orphanRows,
129
+ ...state.orphanOutcomes,
130
+ ].sort((left, right) => left.sequence - right.sequence);
131
+ const frontier = turns.at(-1);
132
+ const openTurn = frontier?.kind === "open" ? frontier : undefined;
133
+ const latestOutcome = rows.findLast((row) => row.kind === "outcome");
134
+ return {
135
+ rows,
136
+ turns,
137
+ retained,
138
+ ...(openTurn === undefined ? {} : { openTurn }),
139
+ ...(latestOutcome === undefined ? {} : { latestOutcome }),
140
+ };
141
+ }
127
142
  /** Fold the retained fact timeline into Turn-owned rows without inventing provider facts. */
128
143
  export function projectTurns(facts, retained = {
129
144
  lowestRetained: facts[0]?.sequence ?? null,
@@ -196,155 +211,6 @@ export function projectTurns(facts, retained = {
196
211
  projectActivityEvent(state, fact);
197
212
  }
198
213
  }
199
- const turns = state.turns.map(finishTurn);
200
- const rows = [
201
- ...turns.flatMap((turn) => [turn.turn, ...turn.rows, ...(turn.kind === "closed" ? [turn.outcome] : [])]),
202
- ...state.tells,
203
- ...state.orphanRows,
204
- ...state.orphanOutcomes,
205
- ].sort((left, right) => left.sequence - right.sequence);
206
- const frontier = turns.at(-1);
207
- const openTurn = frontier?.kind === "open" ? frontier : undefined;
208
- const latestOutcome = rows.findLast((row) => row.kind === "outcome");
209
- return {
210
- rows,
211
- turns,
212
- retained,
213
- ...(openTurn === undefined ? {} : { openTurn }),
214
- ...(latestOutcome === undefined ? {} : { latestOutcome }),
215
- };
216
- }
217
- function assembleEntries(ledger, window, selected) {
218
- const windowRows = new Set(window);
219
- const result = [];
220
- let hidden = 0;
221
- const flushGap = () => {
222
- if (hidden === 0)
223
- return;
224
- result.push({ kind: "gap", count: hidden });
225
- hidden = 0;
226
- };
227
- for (const row of ledger.rows) {
228
- if (windowRows.has(row) && !selected.has(row)) {
229
- hidden += 1;
230
- continue;
231
- }
232
- if (!selected.has(row))
233
- continue;
234
- flushGap();
235
- if (row.kind !== "turn" && row.kind !== "outcome" && !(row.kind === "tool" && row.state === "unsettled")) {
236
- result.push({ kind: "row", row: row });
237
- }
238
- }
239
- flushGap();
240
- return result;
241
- }
242
- const DEFAULT_ORDINARY = DEFAULT_TAIL + DEFAULT_VOICE;
243
- /** Split one nonnegative ordinary allowance into newest tail first, then pre-tail voice. */
244
- export function ordinarySnapshotBudget(ordinaryBudget = DEFAULT_ORDINARY) {
245
- const tail = Math.min(DEFAULT_TAIL, Math.max(0, ordinaryBudget));
246
- return { tail, voice: Math.min(DEFAULT_VOICE, Math.max(0, ordinaryBudget) - tail) };
247
- }
248
- function isSuccessfulFileChange(row) {
249
- return (row.kind === "tool" && typeof row.state === "object" && row.state.status === "ok" && row.call.kind === "fileChange");
250
- }
251
- function flattenReportedChanges(rows) {
252
- return rows.flatMap((row) => {
253
- if (!isSuccessfulFileChange(row))
254
- return [];
255
- return row.call.changes.map((change) => ({
256
- sequence: row.sequence,
257
- at: row.at,
258
- op: change.op,
259
- path: change.path,
260
- ...(change.diffstat === undefined ? {} : { diffstat: change.diffstat }),
261
- }));
262
- });
263
- }
264
- function summarizeReportedChanges(rows) {
265
- const eligible = flattenReportedChanges(rows);
266
- return {
267
- reportedChanges: eligible.slice(-REPORTED_CHANGE_LIMIT),
268
- reportedChangesOmitted: Math.max(0, eligible.length - REPORTED_CHANGE_LIMIT),
269
- };
270
- }
271
- function frontierReportedChanges(ledger) {
272
- if (ledger.openTurn !== undefined)
273
- return summarizeReportedChanges(ledger.openTurn.rows);
274
- const closed = ledger.turns.findLast((turn) => turn.kind === "closed");
275
- return closed === undefined ? EMPTY_REPORTED : summarizeReportedChanges(closed.rows);
276
- }
277
- function isPendingTell(row) {
278
- return row.kind === "tell" && row.state === "pending";
279
- }
280
- function isToldTell(row) {
281
- return row.kind === "tell" && row.state === "told";
282
- }
283
- function isActiveTool(row) {
284
- return row.kind === "tool" && row.state === "active";
285
- }
286
- /** Select one current Turn, one latest outcome, or no focus; pending tells stay actionable. */
287
- export function selectSnapshot(ledger, input) {
288
- const budget = input.budget ?? { tail: DEFAULT_TAIL, voice: DEFAULT_VOICE };
289
- if (ledger.turns.length === 0 && !ledger.rows.some((row) => row.kind === "tell")) {
290
- return { snapshot: { kind: "unborn", entries: [], omitted: 0, ...EMPTY_REPORTED }, ordinaryCount: 0 };
291
- }
292
- const pending = ledger.rows.filter(isPendingTell);
293
- const latestTold = input.aperture === "monitoring" ? ledger.rows.findLast(isToldTell) : undefined;
294
- const pins = latestTold === undefined ? pending : [...pending, latestTold];
295
- const reported = frontierReportedChanges(ledger);
296
- if (ledger.openTurn !== undefined) {
297
- const window = ledger.openTurn.rows;
298
- const pinSet = new Set([...window.filter(isActiveTool), ...pins]);
299
- const ordinary = window.filter((row) => !pinSet.has(row));
300
- const tailCount = Math.max(0, budget.tail);
301
- const tail = tailCount === 0 ? [] : ordinary.slice(-tailCount);
302
- const tailSet = new Set(tail);
303
- const voiceCount = Math.max(0, budget.voice ?? DEFAULT_VOICE);
304
- const voiceCandidates = ordinary.filter((row) => !tailSet.has(row) && (row.kind === "said" || row.kind === "thought"));
305
- const voice = voiceCount === 0 ? [] : voiceCandidates.slice(-voiceCount);
306
- const selected = new Set([...pinSet, ...tail, ...voice]);
307
- const omitted = window.filter((row) => !selected.has(row)).length;
308
- return {
309
- snapshot: {
310
- kind: "open",
311
- turn: ledger.openTurn.turn,
312
- entries: assembleEntries(ledger, window, selected),
313
- omitted,
314
- ...reported,
315
- },
316
- ordinaryCount: tail.length + voice.length,
317
- };
318
- }
319
- return {
320
- snapshot: {
321
- kind: "idle",
322
- ...(ledger.latestOutcome === undefined ? {} : { outcome: ledger.latestOutcome }),
323
- entries: assembleEntries(ledger, [], new Set(pins)),
324
- omitted: 0,
325
- ...reported,
326
- },
327
- ordinaryCount: 0,
328
- };
329
- }
330
- export function selectHistory(ledger, cursor) {
331
- const eligible = ledger.rows.filter((row) => cursor.before !== undefined
332
- ? row.sequence < cursor.before
333
- : cursor.since !== undefined
334
- ? row.sequence > cursor.since
335
- : true);
336
- const rows = cursor.since !== undefined ? eligible.slice(0, cursor.limit) : eligible.slice(-cursor.limit);
337
- const omitted = Math.max(0, eligible.length - rows.length);
338
- const historyLost = cursor.since !== undefined
339
- ? ledger.rows[0] !== undefined && ledger.rows[0].sequence > cursor.since + 1
340
- : ledger.retained.lowestRetained !== null && ledger.retained.lowestRetained > 1 && omitted === 0;
341
- return {
342
- rows,
343
- omitted,
344
- hasEarlier: cursor.since === undefined && eligible.length > rows.length,
345
- hasLater: cursor.since !== undefined && eligible.length > rows.length,
346
- historyLost,
347
- lowestRetained: ledger.retained.lowestRetained,
348
- highest: ledger.retained.highest,
349
- };
214
+ return finishLedger(state, retained);
350
215
  }
216
+ export { ordinarySnapshotBudget, selectHistory, selectSnapshot } from "./projection-read.js";