@astrosheep/keiyaku 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -8
- package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +4 -4
- package/build/integrations/marketplace/plugins/keiyaku/opencode.js +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/package.json +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +13 -5
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +91 -32
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +134 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/agents/openai.yaml +4 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +41 -6
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +262 -46
- package/build/src/akuma/abort.d.ts +2 -0
- package/build/src/akuma/abort.js +50 -0
- package/build/src/akuma/akuma.d.ts +50 -33
- package/build/src/akuma/akuma.js +175 -127
- package/build/src/akuma/archetype.d.ts +14 -5
- package/build/src/akuma/archetype.js +85 -39
- package/build/src/akuma/body.d.ts +3 -5
- package/build/src/akuma/body.js +490 -183
- package/build/src/akuma/coordinate.d.ts +9 -0
- package/build/src/akuma/coordinate.js +22 -0
- package/build/src/akuma/heart/facts.d.ts +88 -45
- package/build/src/akuma/heart/facts.js +19 -8
- package/build/src/akuma/heart/index.d.ts +64 -55
- package/build/src/akuma/heart/index.js +172 -252
- package/build/src/akuma/heart/rows.d.ts +38 -83
- package/build/src/akuma/heart/rows.js +117 -155
- package/build/src/akuma/heart/schema.d.ts +1 -1
- package/build/src/akuma/heart/schema.js +61 -22
- package/build/src/akuma/heart/soul.d.ts +8 -0
- package/build/src/akuma/heart/soul.js +136 -0
- package/build/src/akuma/heart/storage.d.ts +47 -0
- package/build/src/akuma/heart/storage.js +224 -0
- package/build/src/akuma/heart/tells.d.ts +12 -0
- package/build/src/akuma/heart/tells.js +79 -0
- package/build/src/akuma/heart/timeline.d.ts +15 -0
- package/build/src/akuma/heart/timeline.js +86 -0
- package/build/src/akuma/identity.d.ts +2 -2
- package/build/src/akuma/identity.js +9 -9
- package/build/src/akuma/index.d.ts +10 -3
- package/build/src/akuma/index.js +7 -1
- package/build/src/akuma/projection.d.ts +176 -0
- package/build/src/akuma/projection.js +256 -0
- package/build/src/akuma/provider-recipe.d.ts +24 -0
- package/build/src/akuma/provider-recipe.js +100 -0
- package/build/src/akuma/provider.d.ts +67 -13
- package/build/src/akuma/provider.js +172 -105
- package/build/src/akuma/providers/acp/core.d.ts +19 -0
- package/build/src/akuma/providers/acp/core.js +159 -0
- package/build/src/akuma/providers/acp/events.d.ts +20 -0
- package/build/src/akuma/providers/acp/events.js +73 -0
- package/build/src/akuma/providers/acp/index.d.ts +12 -0
- package/build/src/akuma/providers/acp/index.js +94 -0
- package/build/src/akuma/providers/{claude.d.ts → claude/events.d.ts} +9 -41
- package/build/src/akuma/providers/claude/events.js +238 -0
- package/build/src/akuma/providers/claude/index.d.ts +47 -0
- package/build/src/akuma/providers/claude/index.js +279 -0
- package/build/src/akuma/providers/claude/input.d.ts +13 -0
- package/build/src/akuma/providers/claude/input.js +109 -0
- package/build/src/akuma/providers/codex-app-server/events.d.ts +1 -1
- package/build/src/akuma/providers/codex-app-server/events.js +15 -12
- package/build/src/akuma/providers/codex-app-server/index.d.ts +15 -13
- package/build/src/akuma/providers/codex-app-server/index.js +77 -33
- package/build/src/akuma/providers/grok-build/index.d.ts +4 -0
- package/build/src/akuma/providers/grok-build/index.js +70 -0
- package/build/src/akuma/providers/index.d.ts +5 -3
- package/build/src/akuma/providers/index.js +35 -56
- package/build/src/akuma/providers/opencode-sdk/events.d.ts +54 -0
- package/build/src/akuma/providers/opencode-sdk/events.js +235 -0
- package/build/src/akuma/providers/opencode-sdk/index.d.ts +33 -0
- package/build/src/akuma/providers/opencode-sdk/index.js +308 -0
- package/build/src/akuma/providers/opencode-sdk/session.d.ts +26 -0
- package/build/src/akuma/providers/opencode-sdk/session.js +63 -0
- package/build/src/akuma/providers/pi/events.d.ts +33 -0
- package/build/src/akuma/providers/pi/events.js +166 -0
- package/build/src/akuma/providers/pi/index.d.ts +11 -0
- package/build/src/akuma/providers/pi/index.js +191 -0
- package/build/src/akuma/publication.d.ts +3 -3
- package/build/src/akuma/publication.js +28 -40
- package/build/src/akuma/requests.d.ts +9 -6
- package/build/src/akuma/requests.js +95 -65
- package/build/src/alias/index.d.ts +4 -3
- package/build/src/alias/index.js +12 -13
- package/build/src/body/decode.js +7 -1
- package/build/src/body/region.d.ts +2 -0
- package/build/src/body/region.js +77 -7
- package/build/src/body/render.js +2 -1
- package/build/src/body/shape.js +7 -1
- package/build/src/body/verification.js +19 -4
- package/build/src/cli/accepted.d.ts +18 -12
- package/build/src/cli/accepted.js +73 -34
- package/build/src/cli/actor.js +3 -3
- package/build/src/cli/commands/akuma-invoke.d.ts +44 -19
- package/build/src/cli/commands/akuma-invoke.js +101 -52
- package/build/src/cli/commands/akuma.d.ts +27 -9
- package/build/src/cli/commands/akuma.js +151 -80
- package/build/src/cli/commands/contract.d.ts +155 -0
- package/build/src/cli/commands/contract.js +20 -0
- package/build/src/cli/commands/install.js +2 -0
- package/build/src/cli/commands/task-invoke.d.ts +19 -4
- package/build/src/cli/commands/task-invoke.js +65 -17
- package/build/src/cli/commands/task-query.d.ts +5 -0
- package/build/src/cli/commands/task-query.js +247 -0
- package/build/src/cli/commands/task.d.ts +3 -1
- package/build/src/cli/commands/task.js +52 -14
- package/build/src/cli/coordinates.d.ts +18 -0
- package/build/src/cli/coordinates.js +120 -0
- package/build/src/cli/draft.d.ts +8 -0
- package/build/src/cli/draft.js +96 -0
- package/build/src/cli/invoke.d.ts +9 -3
- package/build/src/cli/invoke.js +287 -108
- package/build/src/cli/main.js +54 -7
- package/build/src/cli/parse.d.ts +21 -117
- package/build/src/cli/parse.js +79 -84
- package/build/src/cli/render/akuma-tool-command.d.ts +6 -0
- package/build/src/cli/render/akuma-tool-command.js +144 -0
- package/build/src/cli/render/akuma-tool.d.ts +4 -2
- package/build/src/cli/render/akuma-tool.js +35 -5
- package/build/src/cli/render/akuma.d.ts +3 -1
- package/build/src/cli/render/akuma.js +247 -212
- package/build/src/cli/render/audit.d.ts +3 -0
- package/build/src/cli/render/audit.js +104 -0
- package/build/src/cli/render/catalog.d.ts +2 -0
- package/build/src/cli/render/catalog.js +16 -0
- package/build/src/cli/render/contract.d.ts +3 -3
- package/build/src/cli/render/contract.js +200 -54
- package/build/src/cli/render/kanshi.js +272 -129
- package/build/src/cli/render/receipt.d.ts +19 -0
- package/build/src/cli/render/receipt.js +106 -0
- package/build/src/cli/render/refusal.d.ts +16 -2
- package/build/src/cli/render/refusal.js +88 -3
- package/build/src/cli/render/region.d.ts +2 -0
- package/build/src/cli/render/region.js +17 -0
- package/build/src/cli/render/task.d.ts +2 -1
- package/build/src/cli/render/task.js +223 -60
- package/build/src/cli/render/terminal.d.ts +20 -0
- package/build/src/cli/render/terminal.js +121 -0
- package/build/src/cli/render/text.js +11 -3
- package/build/src/cli/result.d.ts +126 -19
- package/build/src/cli/usage.d.ts +1 -0
- package/build/src/cli/usage.js +3 -0
- package/build/src/contract-worktree.d.ts +44 -0
- package/build/src/contract-worktree.js +251 -0
- package/build/src/coordination/durable-file.d.ts +5 -1
- package/build/src/coordination/durable-file.js +57 -10
- package/build/src/coordination/sqlite-transaction-lock.d.ts +4 -0
- package/build/src/coordination/sqlite-transaction-lock.js +22 -5
- package/build/src/core/facts/codec.js +23 -4
- package/build/src/core/facts/eligibility.d.ts +1 -10
- package/build/src/core/facts/eligibility.js +0 -49
- package/build/src/core/facts/fold.js +1 -6
- package/build/src/core/facts/gate.d.ts +1 -0
- package/build/src/core/facts/gate.js +1 -0
- package/build/src/core/facts/observation.d.ts +3 -0
- package/build/src/core/facts/observation.js +12 -0
- package/build/src/core/facts/types.d.ts +10 -3
- package/build/src/core/subject.js +2 -2
- package/build/src/core/verbs/amend.d.ts +1 -1
- package/build/src/core/verbs/amend.js +3 -18
- package/build/src/core/verbs/bind.js +2 -16
- package/build/src/core/verbs/deliver.d.ts +1 -1
- package/build/src/core/verbs/deliver.js +14 -4
- package/build/src/core/verbs/placement.d.ts +1 -1
- package/build/src/core/verbs/placement.js +7 -9
- package/build/src/dispatch/index.d.ts +5 -3
- package/build/src/dispatch/index.js +30 -21
- package/build/src/duration.d.ts +10 -0
- package/build/src/duration.js +22 -0
- package/build/src/git/admission.d.ts +2 -2
- package/build/src/git/admission.js +33 -23
- package/build/src/git/hooks.d.ts +15 -0
- package/build/src/git/hooks.js +70 -18
- package/build/src/git/identity.d.ts +5 -1
- package/build/src/git/identity.js +23 -4
- package/build/src/git/integration.d.ts +48 -0
- package/build/src/git/integration.js +250 -0
- package/build/src/git/observe.d.ts +29 -15
- package/build/src/git/observe.js +185 -76
- package/build/src/git/read-observation.d.ts +41 -0
- package/build/src/git/read-observation.js +361 -0
- package/build/src/git/reconcile.d.ts +17 -5
- package/build/src/git/reconcile.js +305 -109
- package/build/src/git/repository.d.ts +41 -18
- package/build/src/git/repository.js +220 -114
- package/build/src/git/scratch.d.ts +17 -0
- package/build/src/git/scratch.js +73 -0
- package/build/src/git/target-placement.d.ts +92 -0
- package/build/src/git/target-placement.js +375 -0
- package/build/src/git/tender.d.ts +49 -0
- package/build/src/git/tender.js +63 -0
- package/build/src/git/terminal-seal.d.ts +24 -0
- package/build/src/git/terminal-seal.js +89 -0
- package/build/src/git/tree.d.ts +2 -2
- package/build/src/git/tree.js +4 -0
- package/build/src/git/workspace.d.ts +53 -0
- package/build/src/git/workspace.js +139 -0
- package/build/src/identity/selector.d.ts +6 -0
- package/build/src/identity/selector.js +19 -0
- package/build/src/index.d.ts +4 -2
- package/build/src/index.js +2 -1
- package/build/src/kanshi/index.d.ts +2 -2
- package/build/src/kanshi/index.js +1 -1
- package/build/src/kanshi/read.d.ts +7 -3
- package/build/src/kanshi/read.js +245 -45
- package/build/src/kanshi/report.d.ts +81 -6
- package/build/src/kanshi/select.d.ts +5 -0
- package/build/src/kanshi/select.js +33 -1
- package/build/src/library/address.d.ts +57 -0
- package/build/src/library/address.js +165 -0
- package/build/src/library/akuma-creation.d.ts +16 -7
- package/build/src/library/akuma-creation.js +96 -22
- package/build/src/library/audit.d.ts +18 -0
- package/build/src/library/audit.js +44 -0
- package/build/src/library/bind.d.ts +21 -0
- package/build/src/library/bind.js +56 -0
- package/build/src/library/catalog.d.ts +58 -0
- package/build/src/library/catalog.js +69 -0
- package/build/src/library/configuration.d.ts +6 -15
- package/build/src/library/configuration.js +14 -17
- package/build/src/library/contract.d.ts +23 -49
- package/build/src/library/contract.js +171 -185
- package/build/src/library/delivery.d.ts +24 -0
- package/build/src/library/delivery.js +21 -0
- package/build/src/library/fleet.d.ts +65 -0
- package/build/src/library/fleet.js +153 -0
- package/build/src/library/input.d.ts +3 -0
- package/build/src/library/input.js +19 -0
- package/build/src/library/keiyaku.d.ts +18 -3
- package/build/src/library/keiyaku.js +12 -1
- package/build/src/library/mutation.d.ts +39 -0
- package/build/src/library/mutation.js +37 -0
- package/build/src/library/reconcile.d.ts +29 -0
- package/build/src/library/reconcile.js +179 -0
- package/build/src/library/refusal.d.ts +21 -0
- package/build/src/library/refusal.js +29 -0
- package/build/src/library/region.d.ts +14 -1
- package/build/src/library/region.js +18 -4
- package/build/src/library/repo.d.ts +4 -13
- package/build/src/library/repo.js +14 -24
- package/build/src/protocol/attempt.d.ts +16 -3
- package/build/src/protocol/attempt.js +41 -10
- package/build/src/protocol/bind.d.ts +9 -2
- package/build/src/protocol/bind.js +48 -40
- package/build/src/protocol/intent.d.ts +57 -20
- package/build/src/protocol/intent.js +55 -79
- package/build/src/protocol/operations.d.ts +152 -43
- package/build/src/protocol/operations.js +482 -166
- package/build/src/protocol/outcome.d.ts +14 -3
- package/build/src/protocol/outcome.js +11 -3
- package/build/src/protocol/placement.d.ts +41 -0
- package/build/src/protocol/placement.js +147 -0
- package/build/src/protocol/read/documents.d.ts +2 -2
- package/build/src/protocol/read/documents.js +4 -4
- package/build/src/protocol/read/status.d.ts +18 -5
- package/build/src/protocol/read/status.js +73 -13
- package/build/src/protocol/run.d.ts +49 -7
- package/build/src/protocol/run.js +53 -25
- package/build/src/runtime/proc/line-rpc.d.ts +4 -8
- package/build/src/runtime/proc/line-rpc.js +22 -28
- package/build/src/runtime/proc/run.d.ts +23 -21
- package/build/src/runtime/proc/run.js +123 -89
- package/build/src/runtime/proc/stdio.d.ts +18 -0
- package/build/src/runtime/proc/stdio.js +58 -0
- package/build/src/settings.d.ts +7 -1
- package/build/src/settings.js +32 -17
- package/build/src/settlement/fence.d.ts +5 -0
- package/build/src/settlement/fence.js +14 -0
- package/build/src/settlement/holder.d.ts +35 -4
- package/build/src/settlement/holder.js +127 -28
- package/build/src/settlement/settle.d.ts +9 -1
- package/build/src/settlement/settle.js +74 -37
- package/build/src/task/board.d.ts +23 -5
- package/build/src/task/board.js +17 -10
- package/build/src/task/compose.d.ts +2 -2
- package/build/src/task/compose.js +18 -10
- package/build/src/task/context.d.ts +3 -3
- package/build/src/task/context.js +15 -20
- package/build/src/task/document.d.ts +2 -1
- package/build/src/task/document.js +15 -5
- package/build/src/task/index.d.ts +27 -15
- package/build/src/task/index.js +57 -35
- package/build/src/task/operations.d.ts +38 -13
- package/build/src/task/operations.js +80 -46
- package/build/src/task/query.d.ts +67 -0
- package/build/src/task/query.js +279 -0
- package/build/src/task/store.d.ts +5 -7
- package/build/src/task/store.js +43 -28
- package/build/src/verification/declaration.d.ts +1 -0
- package/build/src/verification/execution.d.ts +51 -0
- package/build/src/verification/execution.js +108 -0
- package/build/src/workspace-place.d.ts +41 -0
- package/build/src/workspace-place.js +386 -0
- package/build/src/world.d.ts +24 -0
- package/build/src/world.js +136 -0
- package/package.json +8 -4
- package/build/src/akuma/activity.d.ts +0 -66
- package/build/src/akuma/activity.js +0 -139
- package/build/src/akuma/providers/claude.js +0 -305
- package/build/src/context-root.d.ts +0 -4
- package/build/src/context-root.js +0 -15
- package/build/src/git/delivery.d.ts +0 -20
- package/build/src/git/delivery.js +0 -109
- package/build/src/git/verification.d.ts +0 -14
- package/build/src/git/verification.js +0 -56
- package/build/src/protocol/read/audit.d.ts +0 -25
- package/build/src/protocol/read/audit.js +0 -39
- package/build/src/verification/producer.d.ts +0 -23
- package/build/src/verification/producer.js +0 -54
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const HEART_SCHEMA_VERSION =
|
|
1
|
+
const HEART_SCHEMA_VERSION = 14;
|
|
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();
|
|
@@ -19,24 +19,16 @@ export const HEART_SCHEMA = `
|
|
|
19
19
|
INSERT OR IGNORE INTO akuma_schema(singleton, version) VALUES (1, ${HEART_SCHEMA_VERSION});
|
|
20
20
|
CREATE TABLE IF NOT EXISTS soul (
|
|
21
21
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
22
|
-
|
|
23
|
-
archetype TEXT NOT NULL,
|
|
24
|
-
description TEXT,
|
|
25
|
-
provider_json TEXT NOT NULL CHECK (json_valid(provider_json)),
|
|
26
|
-
options_json TEXT NOT NULL CHECK (json_valid(options_json)),
|
|
27
|
-
cwd TEXT NOT NULL,
|
|
28
|
-
origin_json TEXT NOT NULL CHECK (json_valid(origin_json)),
|
|
29
|
-
confinement_json TEXT NOT NULL CHECK (json_valid(confinement_json)),
|
|
30
|
-
created_at TEXT NOT NULL
|
|
22
|
+
soul_json TEXT NOT NULL CHECK (json_valid(soul_json))
|
|
31
23
|
) STRICT;
|
|
32
24
|
CREATE TABLE IF NOT EXISTS bodies (
|
|
33
25
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
-
pid INTEGER NOT NULL CHECK (pid > 0),
|
|
35
|
-
process_group INTEGER NOT NULL CHECK (process_group > 0),
|
|
36
|
-
spawned_at TEXT NOT NULL,
|
|
37
26
|
leash_taken_at TEXT NOT NULL,
|
|
27
|
+
hung_diagnostic TEXT,
|
|
28
|
+
hung_at TEXT,
|
|
38
29
|
end TEXT CHECK (end IN ('exited', 'broke-off', 'put-down')),
|
|
39
|
-
ended_at TEXT
|
|
30
|
+
ended_at TEXT,
|
|
31
|
+
CHECK ((hung_diagnostic IS NULL AND hung_at IS NULL) OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL AND end IS NULL))
|
|
40
32
|
) STRICT;
|
|
41
33
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
42
34
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -47,31 +39,72 @@ export const HEART_SCHEMA = `
|
|
|
47
39
|
admitted_at TEXT NOT NULL
|
|
48
40
|
) STRICT;
|
|
49
41
|
CREATE TABLE IF NOT EXISTS turns (
|
|
50
|
-
sequence INTEGER PRIMARY KEY
|
|
42
|
+
sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,
|
|
51
43
|
body_sequence INTEGER NOT NULL REFERENCES bodies(sequence),
|
|
52
|
-
|
|
44
|
+
started_at TEXT NOT NULL,
|
|
45
|
+
end_sequence INTEGER UNIQUE REFERENCES timeline(sequence) ON DELETE SET NULL,
|
|
46
|
+
outcome TEXT CHECK (outcome IN ('answered', 'failed')),
|
|
53
47
|
history_id TEXT UNIQUE,
|
|
54
48
|
session_json TEXT CHECK (session_json IS NULL OR json_valid(session_json)),
|
|
55
49
|
answer TEXT,
|
|
56
50
|
diagnostic TEXT,
|
|
57
|
-
completed_at TEXT
|
|
51
|
+
completed_at TEXT,
|
|
58
52
|
CHECK (
|
|
59
|
-
(outcome = 'answered' AND
|
|
53
|
+
(outcome = 'answered' AND session_json IS NOT NULL AND answer IS NOT NULL AND diagnostic IS NULL)
|
|
60
54
|
OR (outcome = 'failed' AND history_id IS NULL AND session_json IS NULL AND answer IS NULL AND diagnostic IS NOT NULL)
|
|
55
|
+
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)
|
|
61
56
|
)
|
|
62
57
|
) STRICT;
|
|
63
|
-
CREATE TABLE IF NOT EXISTS
|
|
58
|
+
CREATE TABLE IF NOT EXISTS timeline (
|
|
64
59
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
-
|
|
60
|
+
kind TEXT NOT NULL CHECK (kind IN ('turn-start', 'call', 'activity', 'tell', 'turn-end'))
|
|
61
|
+
) STRICT;
|
|
62
|
+
CREATE TABLE IF NOT EXISTS calls (
|
|
63
|
+
sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,
|
|
64
|
+
turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,
|
|
65
|
+
body TEXT NOT NULL,
|
|
66
|
+
at TEXT NOT NULL
|
|
67
|
+
) STRICT;
|
|
68
|
+
CREATE TABLE IF NOT EXISTS activity (
|
|
69
|
+
sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,
|
|
70
|
+
turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,
|
|
66
71
|
event_json TEXT NOT NULL CHECK (json_valid(event_json)),
|
|
67
72
|
at TEXT NOT NULL
|
|
68
73
|
) STRICT;
|
|
69
74
|
CREATE TABLE IF NOT EXISTS tells (
|
|
70
75
|
id TEXT PRIMARY KEY,
|
|
76
|
+
sequence INTEGER NOT NULL UNIQUE REFERENCES timeline(sequence) ON DELETE CASCADE,
|
|
71
77
|
body TEXT NOT NULL,
|
|
72
|
-
state TEXT NOT NULL CHECK (state IN ('recorded', 'delivered', 'seen', 'consumed', 'voided-by-death')),
|
|
73
78
|
recorded_at TEXT NOT NULL
|
|
74
79
|
) STRICT;
|
|
80
|
+
CREATE TABLE IF NOT EXISTS tell_deliveries (
|
|
81
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
tell_id TEXT NOT NULL REFERENCES tells(id) ON DELETE CASCADE,
|
|
83
|
+
route TEXT NOT NULL CHECK (route IN ('launch', 'live')),
|
|
84
|
+
turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,
|
|
85
|
+
fence TEXT NOT NULL,
|
|
86
|
+
receipt TEXT CHECK (receipt IN ('unavailable', 'required')),
|
|
87
|
+
delivered_at TEXT NOT NULL,
|
|
88
|
+
CHECK ((route = 'launch' AND receipt IS NULL) OR (route = 'live' AND receipt IS NOT NULL)),
|
|
89
|
+
UNIQUE (tell_id, turn_sequence, fence)
|
|
90
|
+
) STRICT;
|
|
91
|
+
CREATE TABLE IF NOT EXISTS tell_receipts (
|
|
92
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
evidence TEXT NOT NULL CHECK (evidence IN ('exact', 'fence')),
|
|
94
|
+
tell_id TEXT REFERENCES tells(id) ON DELETE CASCADE,
|
|
95
|
+
turn_sequence INTEGER REFERENCES turns(sequence) ON DELETE CASCADE,
|
|
96
|
+
fence TEXT,
|
|
97
|
+
kind TEXT NOT NULL,
|
|
98
|
+
received_at TEXT NOT NULL,
|
|
99
|
+
CHECK (
|
|
100
|
+
(evidence = 'exact' AND tell_id IS NOT NULL AND turn_sequence IS NULL AND fence IS NULL)
|
|
101
|
+
OR (evidence = 'fence' AND tell_id IS NULL AND turn_sequence IS NOT NULL AND fence IS NOT NULL)
|
|
102
|
+
)
|
|
103
|
+
) STRICT;
|
|
104
|
+
CREATE UNIQUE INDEX IF NOT EXISTS tell_receipts_exact
|
|
105
|
+
ON tell_receipts(tell_id, kind) WHERE evidence = 'exact';
|
|
106
|
+
CREATE UNIQUE INDEX IF NOT EXISTS tell_receipts_fence
|
|
107
|
+
ON tell_receipts(turn_sequence, fence, kind) WHERE evidence = 'fence';
|
|
75
108
|
CREATE TABLE IF NOT EXISTS requests (
|
|
76
109
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
110
|
id TEXT NOT NULL UNIQUE,
|
|
@@ -93,10 +126,16 @@ export const HEART_SCHEMA = `
|
|
|
93
126
|
)
|
|
94
127
|
) STRICT;
|
|
95
128
|
CREATE TABLE IF NOT EXISTS control (
|
|
96
|
-
kind TEXT PRIMARY KEY CHECK (kind IN ('stop', 'pause'
|
|
129
|
+
kind TEXT PRIMARY KEY CHECK (kind IN ('stop', 'pause')),
|
|
97
130
|
value_json TEXT NOT NULL CHECK (json_valid(value_json)),
|
|
98
131
|
at TEXT NOT NULL
|
|
99
132
|
) STRICT;
|
|
133
|
+
CREATE TABLE IF NOT EXISTS kills (
|
|
134
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
135
|
+
body_sequence INTEGER NOT NULL UNIQUE REFERENCES bodies(sequence),
|
|
136
|
+
evidence TEXT NOT NULL CHECK (evidence = 'killed'),
|
|
137
|
+
at TEXT NOT NULL
|
|
138
|
+
) STRICT;
|
|
100
139
|
`;
|
|
101
140
|
export const LEASH_SCHEMA = `
|
|
102
141
|
PRAGMA journal_mode=DELETE;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { Soul, SoulRow } from "./facts.js";
|
|
3
|
+
export declare function decodeSoul(value: unknown): Soul;
|
|
4
|
+
export declare function encodeSoul(soul: Soul): string;
|
|
5
|
+
export declare function encodeSoulRow(soul: Soul): readonly [string];
|
|
6
|
+
export declare function decodeSoulRow(row: SoulRow): Soul;
|
|
7
|
+
export declare function soulFact(database: DatabaseSync): Soul | null;
|
|
8
|
+
export declare function insertSoulFact(database: DatabaseSync, soul: Soul): void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { parseAkuId } from "../identity.js";
|
|
2
|
+
import { decodeProviderOptions, decodeProviderRecipe, decodeReadonlyRestraint, } from "../provider-recipe.js";
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: null;
|
|
7
|
+
}
|
|
8
|
+
function unknownKey(value, allowed) {
|
|
9
|
+
return Object.keys(value).find((key) => !allowed.includes(key));
|
|
10
|
+
}
|
|
11
|
+
function nonblank(value, what) {
|
|
12
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
13
|
+
throw new Error(`Akuma soul ${what} must be a nonblank string`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function soulAkuIdentity(value, what) {
|
|
18
|
+
try {
|
|
19
|
+
return parseAkuId(nonblank(value, what));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new Error(`Akuma soul ${what} is not an Akuma identity coordinate`, { cause: error });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function decodeOrigin(value) {
|
|
26
|
+
const origin = record(value);
|
|
27
|
+
if (origin === null)
|
|
28
|
+
throw new Error("Akuma soul origin must be an object");
|
|
29
|
+
if (origin.kind === "direct") {
|
|
30
|
+
if (unknownKey(origin, ["kind"]) !== undefined)
|
|
31
|
+
throw new Error("Akuma soul direct origin must have only kind");
|
|
32
|
+
return { kind: "direct" };
|
|
33
|
+
}
|
|
34
|
+
if (origin.kind === "request") {
|
|
35
|
+
if (unknownKey(origin, ["kind", "parent", "requestId"]) !== undefined) {
|
|
36
|
+
throw new Error("Akuma soul request origin must have only kind, parent, and requestId");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
kind: "request",
|
|
40
|
+
parent: soulAkuIdentity(origin.parent, "request parent").id,
|
|
41
|
+
requestId: nonblank(origin.requestId, "request requestId"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (origin.kind === "fork") {
|
|
45
|
+
if (unknownKey(origin, ["at", "kind", "parent"]) !== undefined) {
|
|
46
|
+
throw new Error("Akuma soul fork origin must have only kind, parent, and at");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: "fork",
|
|
50
|
+
parent: soulAkuIdentity(origin.parent, "fork parent").id,
|
|
51
|
+
at: nonblank(origin.at, "fork at"),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Akuma soul origin must be direct, request, or fork");
|
|
55
|
+
}
|
|
56
|
+
function decodeConfinement(value) {
|
|
57
|
+
const confinement = record(value);
|
|
58
|
+
if (confinement === null)
|
|
59
|
+
throw new Error("Akuma soul confinement must be an object");
|
|
60
|
+
if (confinement.kind === "unconfined") {
|
|
61
|
+
if (unknownKey(confinement, ["kind"]) !== undefined)
|
|
62
|
+
throw new Error("Akuma soul unconfined confinement must have only kind");
|
|
63
|
+
return { kind: "unconfined" };
|
|
64
|
+
}
|
|
65
|
+
if (confinement.kind === "declared") {
|
|
66
|
+
if (unknownKey(confinement, ["kind", "writableRoots"]) !== undefined) {
|
|
67
|
+
throw new Error("Akuma soul declared confinement must have only kind and writableRoots");
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(confinement.writableRoots)
|
|
70
|
+
|| confinement.writableRoots.some((root) => typeof root !== "string" || root.trim().length === 0)) {
|
|
71
|
+
throw new Error("Akuma soul declared confinement writableRoots must be nonblank strings");
|
|
72
|
+
}
|
|
73
|
+
return { kind: "declared", writableRoots: [...confinement.writableRoots] };
|
|
74
|
+
}
|
|
75
|
+
throw new Error("Akuma soul confinement must be unconfined or declared");
|
|
76
|
+
}
|
|
77
|
+
function validateSoul(value) {
|
|
78
|
+
const soul = record(value);
|
|
79
|
+
if (soul === null)
|
|
80
|
+
throw new Error("Akuma soul must be an object");
|
|
81
|
+
if (unknownKey(soul, ["archetype", "confinement", "createdAt", "cwd", "description", "id", "options", "origin", "provider", "readonly"]) !== undefined) {
|
|
82
|
+
throw new Error("Akuma soul has an unknown field");
|
|
83
|
+
}
|
|
84
|
+
const identity = soulAkuIdentity(soul.id, "id");
|
|
85
|
+
const id = identity.id;
|
|
86
|
+
const archetype = nonblank(soul.archetype, "name");
|
|
87
|
+
if (identity.archetype !== archetype)
|
|
88
|
+
throw new Error("Akuma soul id and name must agree");
|
|
89
|
+
const description = soul.description === undefined ? undefined : nonblank(soul.description, "description");
|
|
90
|
+
const provider = decodeProviderRecipe(soul.provider);
|
|
91
|
+
const options = decodeProviderOptions(soul.options);
|
|
92
|
+
const restraint = soul.readonly === undefined ? undefined : decodeReadonlyRestraint(soul.readonly);
|
|
93
|
+
if ((options.readonly === true) !== (restraint !== undefined)) {
|
|
94
|
+
throw new Error("Akuma soul readonly option and restraint must agree");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
id,
|
|
98
|
+
archetype,
|
|
99
|
+
...(description === undefined ? {} : { description }),
|
|
100
|
+
provider,
|
|
101
|
+
options,
|
|
102
|
+
...(restraint === undefined ? {} : { readonly: restraint }),
|
|
103
|
+
cwd: nonblank(soul.cwd, "cwd"),
|
|
104
|
+
origin: decodeOrigin(soul.origin),
|
|
105
|
+
confinement: decodeConfinement(soul.confinement),
|
|
106
|
+
createdAt: nonblank(soul.createdAt, "createdAt"),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function deepFreeze(value) {
|
|
110
|
+
if (value !== null && typeof value === "object") {
|
|
111
|
+
for (const key of Object.keys(value)) {
|
|
112
|
+
deepFreeze(value[key]);
|
|
113
|
+
}
|
|
114
|
+
Object.freeze(value);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
export function decodeSoul(value) {
|
|
119
|
+
return deepFreeze(validateSoul(value));
|
|
120
|
+
}
|
|
121
|
+
export function encodeSoul(soul) {
|
|
122
|
+
return JSON.stringify(validateSoul(soul));
|
|
123
|
+
}
|
|
124
|
+
export function encodeSoulRow(soul) {
|
|
125
|
+
return [encodeSoul(soul)];
|
|
126
|
+
}
|
|
127
|
+
export function decodeSoulRow(row) {
|
|
128
|
+
return decodeSoul(JSON.parse(row.soul_json));
|
|
129
|
+
}
|
|
130
|
+
export function soulFact(database) {
|
|
131
|
+
const row = database.prepare("SELECT soul_json FROM soul WHERE singleton = 1").get();
|
|
132
|
+
return row === undefined ? null : decodeSoulRow(row);
|
|
133
|
+
}
|
|
134
|
+
export function insertSoulFact(database, soul) {
|
|
135
|
+
database.prepare("INSERT INTO soul(singleton, soul_json) VALUES (1, ?)").run(...encodeSoulRow(soul));
|
|
136
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { AkumaPaths } from "../identity.js";
|
|
3
|
+
import type { BodyFact, LeashProbe, SessionFact, Soul, StopFact, TellFact } from "./facts.js";
|
|
4
|
+
import { sealFact } from "./rows.js";
|
|
5
|
+
export declare class HeartAbsentError extends Error {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
constructor(path: string, options?: ErrorOptions);
|
|
8
|
+
}
|
|
9
|
+
export declare function isHeartAbsent(error: unknown): error is HeartAbsentError;
|
|
10
|
+
export declare function withHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
|
|
11
|
+
export declare function transaction<T>(database: DatabaseSync, body: () => T): T;
|
|
12
|
+
export declare function initializeHeart(paths: AkumaPaths): Promise<void>;
|
|
13
|
+
export declare class HeldAkumaLeash {
|
|
14
|
+
private readonly database;
|
|
15
|
+
private closed;
|
|
16
|
+
private bodySequence;
|
|
17
|
+
private constructor();
|
|
18
|
+
static try(paths: AkumaPaths): Promise<HeldAkumaLeash | null>;
|
|
19
|
+
birth(paths: AkumaPaths, soul: Soul, session?: Omit<SessionFact, "sequence">): Promise<"born" | "already-born" | "sealed">;
|
|
20
|
+
sealIfUnborn(paths: AkumaPaths, input: Readonly<{
|
|
21
|
+
evidence: string;
|
|
22
|
+
at: string;
|
|
23
|
+
}>): Promise<"born" | "sealed">;
|
|
24
|
+
clearPause(paths: AkumaPaths): Promise<void>;
|
|
25
|
+
clearStop(paths: AkumaPaths): Promise<void>;
|
|
26
|
+
recordBody(paths: AkumaPaths, input: Readonly<{
|
|
27
|
+
leashTakenAt: string;
|
|
28
|
+
}>): Promise<BodyFact>;
|
|
29
|
+
recordBodyHung(paths: AkumaPaths, input: Readonly<{
|
|
30
|
+
sequence: number;
|
|
31
|
+
diagnostic: string;
|
|
32
|
+
at: string;
|
|
33
|
+
}>): Promise<void>;
|
|
34
|
+
settleStop(paths: AkumaPaths, expectedBodySequence?: number): Promise<Readonly<{
|
|
35
|
+
target: StopFact;
|
|
36
|
+
result: "recorded" | "already-killed";
|
|
37
|
+
}> | null>;
|
|
38
|
+
recordInterruptTell(paths: AkumaPaths, tell: Omit<TellFact, "sequence" | "state" | "deliveries">): Promise<Readonly<{
|
|
39
|
+
kind: "not-born";
|
|
40
|
+
} | {
|
|
41
|
+
kind: "recorded";
|
|
42
|
+
tell: TellFact;
|
|
43
|
+
}>>;
|
|
44
|
+
release(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function probeLeash(paths: AkumaPaths): Promise<LeashProbe>;
|
|
47
|
+
export declare function readSealFromLeash(paths: AkumaPaths): Promise<ReturnType<typeof sealFact>>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion } from "./schema.js";
|
|
5
|
+
import { deletePauseControl, deleteStopControl, insertBodyFact, insertKillFact, insertSealFact, insertSessionFact, killFactForBody, latestBodyFact, latestKillFact, markBodyHung, sealExists, sealFact, stopFact, } from "./rows.js";
|
|
6
|
+
import { insertTellFact } from "./tells.js";
|
|
7
|
+
import { pruneActivityFacts } from "./timeline.js";
|
|
8
|
+
import { insertSoulFact, soulFact } from "./soul.js";
|
|
9
|
+
const ACTIVITY_LIMIT = 5_000;
|
|
10
|
+
const SQLITE_CANTOPEN = 14;
|
|
11
|
+
export class HeartAbsentError extends Error {
|
|
12
|
+
path;
|
|
13
|
+
constructor(path, options) {
|
|
14
|
+
super(`Akuma Heart database is absent: ${path}`, options);
|
|
15
|
+
this.path = path;
|
|
16
|
+
this.name = "HeartAbsentError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isHeartAbsent(error) {
|
|
20
|
+
return error instanceof HeartAbsentError;
|
|
21
|
+
}
|
|
22
|
+
function isBusy(error) {
|
|
23
|
+
const value = error;
|
|
24
|
+
const message = String(value?.message ?? "").toLowerCase();
|
|
25
|
+
return value?.code === "ERR_SQLITE_BUSY" || value?.code === "ERR_SQLITE_LOCKED"
|
|
26
|
+
|| value?.errcode === 5 || message.includes("database is locked") || message.includes("database is busy");
|
|
27
|
+
}
|
|
28
|
+
async function openExistingDatabase(path, timeout) {
|
|
29
|
+
const uri = pathToFileURL(path);
|
|
30
|
+
uri.searchParams.set("mode", "rw");
|
|
31
|
+
try {
|
|
32
|
+
return timeout === undefined
|
|
33
|
+
? new DatabaseSync(uri.href)
|
|
34
|
+
: new DatabaseSync(uri.href, { timeout });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.errcode === SQLITE_CANTOPEN) {
|
|
38
|
+
try {
|
|
39
|
+
await lstat(path);
|
|
40
|
+
}
|
|
41
|
+
catch (metadataError) {
|
|
42
|
+
if (metadataError.code === "ENOENT") {
|
|
43
|
+
throw new HeartAbsentError(path, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function openHeart(path, verify = true) {
|
|
51
|
+
const database = await openExistingDatabase(path);
|
|
52
|
+
try {
|
|
53
|
+
database.exec("PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL");
|
|
54
|
+
if (verify)
|
|
55
|
+
assertHeartSchemaVersion(database);
|
|
56
|
+
return database;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
database.close();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function withHeart(paths, body) {
|
|
64
|
+
const database = await openHeart(paths.heart);
|
|
65
|
+
try {
|
|
66
|
+
return body(database);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
database.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function transaction(database, body) {
|
|
73
|
+
database.exec("BEGIN IMMEDIATE");
|
|
74
|
+
try {
|
|
75
|
+
const result = body();
|
|
76
|
+
database.exec("COMMIT");
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
try {
|
|
81
|
+
database.exec("ROLLBACK");
|
|
82
|
+
}
|
|
83
|
+
catch { /* preserve the adjudication failure */ }
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export async function initializeHeart(paths) {
|
|
88
|
+
const heart = new DatabaseSync(paths.heart);
|
|
89
|
+
try {
|
|
90
|
+
heart.exec(HEART_SCHEMA);
|
|
91
|
+
assertHeartSchemaVersion(heart);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
heart.close();
|
|
95
|
+
}
|
|
96
|
+
const leash = new DatabaseSync(paths.leash);
|
|
97
|
+
try {
|
|
98
|
+
leash.exec(LEASH_SCHEMA);
|
|
99
|
+
assertLeashSchemaVersion(leash);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
leash.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export class HeldAkumaLeash {
|
|
106
|
+
database;
|
|
107
|
+
closed = false;
|
|
108
|
+
bodySequence;
|
|
109
|
+
constructor(database) {
|
|
110
|
+
this.database = database;
|
|
111
|
+
}
|
|
112
|
+
static async try(paths) {
|
|
113
|
+
const database = await openExistingDatabase(paths.leash, 0);
|
|
114
|
+
try {
|
|
115
|
+
assertLeashSchemaVersion(database);
|
|
116
|
+
database.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE");
|
|
117
|
+
return new HeldAkumaLeash(database);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
database.close();
|
|
121
|
+
if (isBusy(error))
|
|
122
|
+
return null;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async birth(paths, soul, session) {
|
|
127
|
+
if (sealExists(this.database))
|
|
128
|
+
return "sealed";
|
|
129
|
+
return await withHeart(paths, (heart) => transaction(heart, () => {
|
|
130
|
+
if (soulFact(heart) !== null)
|
|
131
|
+
return "already-born";
|
|
132
|
+
insertSoulFact(heart, soul);
|
|
133
|
+
if (session !== undefined)
|
|
134
|
+
insertSessionFact(heart, session);
|
|
135
|
+
return "born";
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
async sealIfUnborn(paths, input) {
|
|
139
|
+
if (await withHeart(paths, (heart) => soulFact(heart)) !== null)
|
|
140
|
+
return "born";
|
|
141
|
+
insertSealFact(this.database, input);
|
|
142
|
+
this.database.exec("COMMIT");
|
|
143
|
+
this.closed = true;
|
|
144
|
+
this.database.close();
|
|
145
|
+
return "sealed";
|
|
146
|
+
}
|
|
147
|
+
async clearPause(paths) { await withHeart(paths, deletePauseControl); }
|
|
148
|
+
async clearStop(paths) { await withHeart(paths, deleteStopControl); }
|
|
149
|
+
async recordBody(paths, input) {
|
|
150
|
+
if (this.closed || this.bodySequence !== undefined)
|
|
151
|
+
throw new Error("Akuma leash cannot start another Body");
|
|
152
|
+
const sequence = await withHeart(paths, (heart) => insertBodyFact(heart, input));
|
|
153
|
+
this.bodySequence = sequence;
|
|
154
|
+
return { sequence, leashTakenAt: input.leashTakenAt };
|
|
155
|
+
}
|
|
156
|
+
async recordBodyHung(paths, input) {
|
|
157
|
+
if (this.closed || this.bodySequence !== input.sequence) {
|
|
158
|
+
throw new Error(`Akuma Body ${input.sequence} is not owned by this leash`);
|
|
159
|
+
}
|
|
160
|
+
await withHeart(paths, (heart) => markBodyHung(heart, input));
|
|
161
|
+
}
|
|
162
|
+
async settleStop(paths, expectedBodySequence) {
|
|
163
|
+
return await withHeart(paths, (heart) => transaction(heart, () => {
|
|
164
|
+
const target = stopFact(heart);
|
|
165
|
+
if (target === null) {
|
|
166
|
+
if (expectedBodySequence === undefined)
|
|
167
|
+
return null;
|
|
168
|
+
const existing = killFactForBody(heart, expectedBodySequence);
|
|
169
|
+
return existing === null ? null : { target: { bodySequence: expectedBodySequence, requestedAt: existing.at }, result: "already-killed" };
|
|
170
|
+
}
|
|
171
|
+
if (expectedBodySequence !== undefined && target.bodySequence !== expectedBodySequence) {
|
|
172
|
+
throw new Error("Akuma stop target changed while kill was in progress");
|
|
173
|
+
}
|
|
174
|
+
const latest = latestBodyFact(heart);
|
|
175
|
+
if (latest?.sequence !== target.bodySequence)
|
|
176
|
+
throw new Error("Akuma stop target is not the latest Body");
|
|
177
|
+
if (latest.end !== "put-down")
|
|
178
|
+
return null;
|
|
179
|
+
const result = latestKillFact(heart)?.bodySequence === target.bodySequence
|
|
180
|
+
? "already-killed"
|
|
181
|
+
: (insertKillFact(heart, target.bodySequence, target.requestedAt), "recorded");
|
|
182
|
+
deleteStopControl(heart);
|
|
183
|
+
return { target, result };
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
async recordInterruptTell(paths, tell) {
|
|
187
|
+
return await withHeart(paths, (heart) => transaction(heart, () => {
|
|
188
|
+
deletePauseControl(heart);
|
|
189
|
+
if (soulFact(heart) === null)
|
|
190
|
+
return { kind: "not-born" };
|
|
191
|
+
const sequence = insertTellFact(heart, tell);
|
|
192
|
+
pruneActivityFacts(heart, ACTIVITY_LIMIT);
|
|
193
|
+
return { kind: "recorded", tell: { sequence, ...tell, state: "pending", deliveries: [] } };
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
release() {
|
|
197
|
+
if (this.closed)
|
|
198
|
+
return;
|
|
199
|
+
this.closed = true;
|
|
200
|
+
try {
|
|
201
|
+
this.database.exec("ROLLBACK");
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
this.database.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export async function probeLeash(paths) {
|
|
209
|
+
const claim = await HeldAkumaLeash.try(paths);
|
|
210
|
+
if (claim === null)
|
|
211
|
+
return "held";
|
|
212
|
+
claim.release();
|
|
213
|
+
return "free";
|
|
214
|
+
}
|
|
215
|
+
export async function readSealFromLeash(paths) {
|
|
216
|
+
const leash = await openExistingDatabase(paths.leash);
|
|
217
|
+
try {
|
|
218
|
+
assertLeashSchemaVersion(leash);
|
|
219
|
+
return sealFact(leash);
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
leash.close();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { TellDeliveryInput, TellFact, TellReceiptInput } from "./facts.js";
|
|
3
|
+
export declare const tellStateSql = "CASE\n WHEN EXISTS (\n SELECT 1 FROM tell_deliveries d\n WHERE d.tell_id = tells.id AND (d.route = 'launch' OR d.receipt = 'unavailable')\n ) THEN 'told'\n WHEN EXISTS (SELECT 1 FROM tell_receipts r WHERE r.evidence = 'exact' AND r.tell_id = tells.id) THEN 'told'\n WHEN EXISTS (\n SELECT 1 FROM tell_receipts r\n JOIN tell_deliveries d ON d.turn_sequence = r.turn_sequence AND d.fence = r.fence\n WHERE r.evidence = 'fence' AND d.tell_id = tells.id\n ) THEN 'told'\n ELSE 'pending'\nEND";
|
|
4
|
+
export declare const pendingTellSequencesSql = "SELECT tells.sequence FROM tells WHERE CASE\n WHEN EXISTS (\n SELECT 1 FROM tell_deliveries d\n WHERE d.tell_id = tells.id AND (d.route = 'launch' OR d.receipt = 'unavailable')\n ) THEN 'told'\n WHEN EXISTS (SELECT 1 FROM tell_receipts r WHERE r.evidence = 'exact' AND r.tell_id = tells.id) THEN 'told'\n WHEN EXISTS (\n SELECT 1 FROM tell_receipts r\n JOIN tell_deliveries d ON d.turn_sequence = r.turn_sequence AND d.fence = r.fence\n WHERE r.evidence = 'fence' AND d.tell_id = tells.id\n ) THEN 'told'\n ELSE 'pending'\nEND = 'pending'";
|
|
5
|
+
export declare const pendingTellProtectionSql = "SELECT d.turn_sequence FROM tell_deliveries d\n JOIN tells ON tells.id = d.tell_id WHERE CASE\n WHEN EXISTS (\n SELECT 1 FROM tell_deliveries d\n WHERE d.tell_id = tells.id AND (d.route = 'launch' OR d.receipt = 'unavailable')\n ) THEN 'told'\n WHEN EXISTS (SELECT 1 FROM tell_receipts r WHERE r.evidence = 'exact' AND r.tell_id = tells.id) THEN 'told'\n WHEN EXISTS (\n SELECT 1 FROM tell_receipts r\n JOIN tell_deliveries d ON d.turn_sequence = r.turn_sequence AND d.fence = r.fence\n WHERE r.evidence = 'fence' AND d.tell_id = tells.id\n ) THEN 'told'\n ELSE 'pending'\nEND = 'pending'";
|
|
6
|
+
export declare function decodeTellAtSequence(database: DatabaseSync, sequence: number): TellFact;
|
|
7
|
+
export declare function insertTellFact(database: DatabaseSync, tell: Omit<TellFact, "sequence" | "state" | "deliveries">): number;
|
|
8
|
+
export declare function tellFact(database: DatabaseSync, id: string): TellFact | null;
|
|
9
|
+
export declare function insertTellDeliveryFact(database: DatabaseSync, input: TellDeliveryInput): void;
|
|
10
|
+
export declare function insertTellReceiptFact(database: DatabaseSync, input: TellReceiptInput): void;
|
|
11
|
+
export declare function tellIdsForFence(database: DatabaseSync, turnSequence: number, fence: string): readonly string[];
|
|
12
|
+
export declare function pendingTellFacts(database: DatabaseSync): readonly TellFact[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const tellStateSql = `CASE
|
|
2
|
+
WHEN EXISTS (
|
|
3
|
+
SELECT 1 FROM tell_deliveries d
|
|
4
|
+
WHERE d.tell_id = tells.id AND (d.route = 'launch' OR d.receipt = 'unavailable')
|
|
5
|
+
) THEN 'told'
|
|
6
|
+
WHEN EXISTS (SELECT 1 FROM tell_receipts r WHERE r.evidence = 'exact' AND r.tell_id = tells.id) THEN 'told'
|
|
7
|
+
WHEN EXISTS (
|
|
8
|
+
SELECT 1 FROM tell_receipts r
|
|
9
|
+
JOIN tell_deliveries d ON d.turn_sequence = r.turn_sequence AND d.fence = r.fence
|
|
10
|
+
WHERE r.evidence = 'fence' AND d.tell_id = tells.id
|
|
11
|
+
) THEN 'told'
|
|
12
|
+
ELSE 'pending'
|
|
13
|
+
END`;
|
|
14
|
+
export const pendingTellSequencesSql = `SELECT tells.sequence FROM tells WHERE ${tellStateSql} = 'pending'`;
|
|
15
|
+
export const pendingTellProtectionSql = `SELECT d.turn_sequence FROM tell_deliveries d
|
|
16
|
+
JOIN tells ON tells.id = d.tell_id WHERE ${tellStateSql} = 'pending'`;
|
|
17
|
+
function tellDeliveries(database, id) {
|
|
18
|
+
const rows = database.prepare(`SELECT turn_sequence, route, receipt, delivered_at FROM tell_deliveries
|
|
19
|
+
WHERE tell_id = ? ORDER BY sequence`).all(id);
|
|
20
|
+
return rows.map((row) => row.receipt == null
|
|
21
|
+
? { turnSequence: row.turn_sequence, route: row.route, deliveredAt: row.delivered_at }
|
|
22
|
+
: { turnSequence: row.turn_sequence, route: row.route, receipt: row.receipt, deliveredAt: row.delivered_at });
|
|
23
|
+
}
|
|
24
|
+
function decodeTellRow(database, row) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "tell",
|
|
27
|
+
sequence: row.sequence,
|
|
28
|
+
id: row.id,
|
|
29
|
+
body: row.body,
|
|
30
|
+
state: row.state,
|
|
31
|
+
recordedAt: row.recorded_at,
|
|
32
|
+
deliveries: tellDeliveries(database, row.id),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function decodeTellAtSequence(database, sequence) {
|
|
36
|
+
const row = database.prepare(`SELECT sequence, id, body, recorded_at, ${tellStateSql} AS state
|
|
37
|
+
FROM tells WHERE sequence = ?`).get(sequence);
|
|
38
|
+
if (row === undefined)
|
|
39
|
+
throw new Error(`Akuma timeline references missing tell ${sequence}`);
|
|
40
|
+
return decodeTellRow(database, row);
|
|
41
|
+
}
|
|
42
|
+
export function insertTellFact(database, tell) {
|
|
43
|
+
const sequence = Number(database.prepare("INSERT INTO timeline(kind) VALUES ('tell')").run().lastInsertRowid);
|
|
44
|
+
database.prepare("INSERT INTO tells(id, sequence, body, recorded_at) VALUES (?, ?, ?, ?)")
|
|
45
|
+
.run(tell.id, sequence, tell.body, tell.recordedAt);
|
|
46
|
+
return sequence;
|
|
47
|
+
}
|
|
48
|
+
export function tellFact(database, id) {
|
|
49
|
+
const row = database.prepare(`SELECT sequence, id, body, recorded_at, ${tellStateSql} AS state
|
|
50
|
+
FROM tells WHERE id = ?`).get(id);
|
|
51
|
+
return row === undefined ? null : decodeTellRow(database, row);
|
|
52
|
+
}
|
|
53
|
+
export function insertTellDeliveryFact(database, input) {
|
|
54
|
+
const result = database.prepare(`INSERT OR IGNORE INTO tell_deliveries(
|
|
55
|
+
tell_id, route, turn_sequence, fence, receipt, delivered_at
|
|
56
|
+
) VALUES (?, ?, ?, ?, ?, ?)`).run(input.tellId, input.route, input.turnSequence, input.fence, input.route === "live" ? input.receipt : null, input.deliveredAt);
|
|
57
|
+
const row = database.prepare(`SELECT route, receipt FROM tell_deliveries
|
|
58
|
+
WHERE tell_id = ? AND turn_sequence = ? AND fence = ?`)
|
|
59
|
+
.get(input.tellId, input.turnSequence, input.fence);
|
|
60
|
+
if (row === undefined || (result.changes === 0
|
|
61
|
+
&& (row.route !== input.route || row.receipt !== (input.route === "live" ? input.receipt : null)))) {
|
|
62
|
+
throw new Error(`tell delivery ${input.tellId} has conflicting evidence`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function insertTellReceiptFact(database, input) {
|
|
66
|
+
database.prepare(`INSERT OR IGNORE INTO tell_receipts(
|
|
67
|
+
evidence, tell_id, turn_sequence, fence, kind, received_at
|
|
68
|
+
) VALUES (?, ?, ?, ?, ?, ?)`).run(input.evidence, input.evidence === "exact" ? input.tellId : null, input.evidence === "fence" ? input.turnSequence : null, input.evidence === "fence" ? input.fence : null, input.kind, input.receivedAt);
|
|
69
|
+
}
|
|
70
|
+
export function tellIdsForFence(database, turnSequence, fence) {
|
|
71
|
+
return database.prepare(`SELECT tell_id FROM tell_deliveries
|
|
72
|
+
WHERE turn_sequence = ? AND fence = ? ORDER BY sequence`)
|
|
73
|
+
.all(turnSequence, fence).map((row) => row.tell_id);
|
|
74
|
+
}
|
|
75
|
+
export function pendingTellFacts(database) {
|
|
76
|
+
const rows = database.prepare(`SELECT sequence, id, body, recorded_at, ${tellStateSql} AS state FROM tells
|
|
77
|
+
WHERE ${tellStateSql} = 'pending' ORDER BY sequence`).all();
|
|
78
|
+
return rows.map((row) => decodeTellRow(database, row));
|
|
79
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { CallFact, TellFact, TurnEndFact, TurnStartFact } from "./facts.js";
|
|
3
|
+
import { type ActivityFact } from "./rows.js";
|
|
4
|
+
export type TimelineFact = TurnStartFact | CallFact | ActivityFact | TellFact | TurnEndFact;
|
|
5
|
+
export type ActivityFactSlice = Readonly<{
|
|
6
|
+
rows: readonly TimelineFact[];
|
|
7
|
+
lowestRetained: number | null;
|
|
8
|
+
highest: number | null;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function pruneActivityFacts(database: DatabaseSync, limit: number): void;
|
|
11
|
+
export declare function activityFactSlice(database: DatabaseSync, input: Readonly<{
|
|
12
|
+
before?: number;
|
|
13
|
+
since?: number;
|
|
14
|
+
limit: number;
|
|
15
|
+
}>): ActivityFactSlice;
|