@astrosheep/keiyaku 4.0.5 → 4.0.6
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/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +15 -7
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +5 -3
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +10 -4
- package/build/src/akuma/akuma.d.ts +2 -0
- package/build/src/akuma/akuma.js +26 -3
- package/build/src/akuma/archetype.d.ts +3 -1
- package/build/src/akuma/archetype.js +11 -6
- package/build/src/akuma/body-turn.d.ts +64 -0
- package/build/src/akuma/body-turn.js +449 -0
- package/build/src/akuma/body.d.ts +3 -7
- package/build/src/akuma/body.js +9 -420
- package/build/src/akuma/heart/facts.d.ts +2 -0
- package/build/src/akuma/heart/facts.js +3 -1
- package/build/src/akuma/heart/index.js +12 -2
- package/build/src/akuma/heart/rows.js +4 -1
- package/build/src/akuma/heart/schema.d.ts +1 -1
- package/build/src/akuma/heart/schema.js +3 -2
- package/build/src/akuma/heart/timeline.d.ts +1 -0
- package/build/src/akuma/heart/timeline.js +19 -0
- package/build/src/akuma/nuke.d.ts +2 -0
- package/build/src/akuma/nuke.js +152 -0
- package/build/src/akuma/provider.d.ts +3 -1
- package/build/src/akuma/providers/acp/config.d.ts +8 -0
- package/build/src/akuma/providers/acp/config.js +37 -0
- package/build/src/akuma/providers/acp/core.js +6 -0
- package/build/src/akuma/providers/acp/index.d.ts +1 -8
- package/build/src/akuma/providers/acp/index.js +2 -37
- package/build/src/akuma/providers/claude/index.js +5 -0
- package/build/src/akuma/providers/codex-app-server/index.js +5 -0
- package/build/src/akuma/providers/grok-build/index.js +1 -1
- package/build/src/akuma/providers/index.d.ts +3 -2
- package/build/src/akuma/providers/index.js +36 -36
- package/build/src/akuma/providers/opencode-sdk/index.js +6 -0
- package/build/src/akuma/providers/opencode-sdk/session.d.ts +1 -1
- package/build/src/akuma/providers/opencode-sdk/session.js +1 -1
- package/build/src/akuma/providers/pi/index.d.ts +1 -1
- package/build/src/akuma/providers/pi/index.js +27 -11
- package/build/src/akuma/request-serve.d.ts +1 -0
- package/build/src/akuma/request-serve.js +31 -31
- package/build/src/akuma/request-wire.d.ts +3 -2
- package/build/src/akuma/request-wire.js +5 -2
- package/build/src/akuma-body.js +15 -1
- package/build/src/alias/index.d.ts +1 -0
- package/build/src/alias/index.js +34 -1
- package/build/src/cli/accepted.js +4 -0
- package/build/src/cli/commands/akuma-invoke.js +1 -0
- package/build/src/cli/commands/akuma.d.ts +1 -0
- package/build/src/cli/commands/akuma.js +11 -8
- package/build/src/cli/commands/contract.d.ts +25 -14
- package/build/src/cli/commands/contract.js +23 -17
- package/build/src/cli/commands/install.d.ts +2 -2
- package/build/src/cli/commands/install.js +6 -6
- package/build/src/cli/commands/nuke.d.ts +10 -0
- package/build/src/cli/commands/nuke.js +20 -0
- package/build/src/cli/commands/task-invoke.d.ts +15 -0
- package/build/src/cli/commands/task-invoke.js +57 -26
- package/build/src/cli/commands/task-query.d.ts +2 -2
- package/build/src/cli/commands/task-query.js +1 -2
- package/build/src/cli/commands/task.js +22 -22
- package/build/src/cli/coordinates.d.ts +5 -2
- package/build/src/cli/coordinates.js +5 -52
- package/build/src/cli/invoke.d.ts +5 -4
- package/build/src/cli/invoke.js +89 -59
- package/build/src/cli/main.d.ts +0 -2
- package/build/src/cli/main.js +4 -140
- package/build/src/cli/parse.d.ts +15 -2
- package/build/src/cli/parse.js +73 -8
- package/build/src/cli/render/akuma.js +55 -40
- package/build/src/cli/render/contract.js +49 -48
- package/build/src/cli/render/kanshi.js +15 -5
- package/build/src/cli/render/nuke.d.ts +3 -0
- package/build/src/cli/render/nuke.js +8 -0
- package/build/src/cli/render/refusal.d.ts +2 -1
- package/build/src/cli/render/refusal.js +31 -0
- package/build/src/cli/render/text.js +6 -1
- package/build/src/cli/result.d.ts +21 -2
- package/build/src/cli/runtime.d.ts +2 -0
- package/build/src/cli/runtime.js +140 -0
- package/build/src/cli/usage.d.ts +0 -2
- package/build/src/cli/usage.js +0 -4
- package/build/src/contract-worktree.d.ts +28 -12
- package/build/src/contract-worktree.js +129 -63
- package/build/src/git/hooks.d.ts +2 -0
- package/build/src/git/hooks.js +34 -1
- package/build/src/git/integration.d.ts +4 -0
- package/build/src/git/integration.js +38 -0
- package/build/src/git/nuke.d.ts +2 -0
- package/build/src/git/nuke.js +97 -0
- package/build/src/git/process.d.ts +2 -0
- package/build/src/git/reconcile.d.ts +2 -0
- package/build/src/git/reconcile.js +33 -2
- package/build/src/git/repository.js +4 -1
- package/build/src/git/target-placement.d.ts +2 -1
- package/build/src/git/target-placement.js +6 -7
- package/build/src/git/tender.d.ts +1 -0
- package/build/src/git/tender.js +2 -2
- package/build/src/git/workspace.d.ts +3 -0
- package/build/src/kanshi/read.js +5 -1
- package/build/src/library/akuma-creation.d.ts +1 -0
- package/build/src/library/akuma-creation.js +8 -1
- package/build/src/library/audit.d.ts +1 -0
- package/build/src/library/audit.js +1 -0
- package/build/src/library/contract.d.ts +6 -4
- package/build/src/library/contract.js +56 -34
- package/build/src/library/delivery.d.ts +3 -1
- package/build/src/library/keiyaku.d.ts +5 -1
- package/build/src/library/keiyaku.js +2 -0
- package/build/src/library/mutation.d.ts +1 -0
- package/build/src/library/mutation.js +3 -0
- package/build/src/library/nuke.d.ts +14 -0
- package/build/src/library/nuke.js +54 -0
- package/build/src/library/reconcile.js +5 -2
- package/build/src/library/refusal.d.ts +10 -1
- package/build/src/library/repo.d.ts +1 -0
- package/build/src/library/repo.js +2 -0
- package/build/src/protocol/audit.js +12 -5
- package/build/src/protocol/completion.d.ts +11 -1
- package/build/src/protocol/completion.js +68 -15
- package/build/src/protocol/deliver.d.ts +14 -2
- package/build/src/protocol/deliver.js +107 -6
- package/build/src/protocol/intent.d.ts +1 -0
- package/build/src/protocol/intent.js +7 -1
- package/build/src/protocol/operations.d.ts +30 -3
- package/build/src/protocol/operations.js +2 -2
- package/build/src/protocol/placement.d.ts +9 -1
- package/build/src/protocol/placement.js +5 -4
- package/build/src/protocol/read/status.d.ts +16 -4
- package/build/src/protocol/read/status.js +54 -12
- package/build/src/protocol/review.d.ts +1 -0
- package/build/src/protocol/review.js +8 -0
- package/build/src/runtime/proc/run.js +9 -5
- package/build/src/settlement/fence.d.ts +2 -0
- package/build/src/settlement/fence.js +24 -0
- package/build/src/settlement/settle.js +1 -2
- package/build/src/task/compose.d.ts +1 -1
- package/build/src/task/compose.js +2 -7
- package/build/src/task/context.d.ts +14 -2
- package/build/src/task/context.js +30 -7
- package/build/src/task/index.d.ts +6 -5
- package/build/src/task/index.js +15 -18
- package/build/src/task/mutation.d.ts +7 -2
- package/build/src/task/mutation.js +14 -6
- package/build/src/task/operations.d.ts +12 -6
- package/build/src/task/operations.js +22 -46
- package/build/src/task/store.d.ts +1 -0
- package/build/src/task/store.js +91 -1
- package/build/src/workspace-place.d.ts +2 -0
- package/build/src/workspace-place.js +27 -1
- package/package.json +1 -1
package/build/src/akuma/body.js
CHANGED
|
@@ -1,159 +1,20 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { abortableDelay } from "./abort.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { BodySupervisor, driveTurn, turnRecipe } from "./body-turn.js";
|
|
4
|
+
import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, } from "./heart/index.js";
|
|
5
5
|
import { resolveProviderExecution } from "./providers/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { clearBodyRequestTransport, settleBodyRequests, } from "./request-serve.js";
|
|
7
7
|
import { spawnDetachedProcess, } from "../runtime/proc/run.js";
|
|
8
8
|
const LEASH_RETRY_MS = 25;
|
|
9
|
-
|
|
10
|
-
export const CONTROL_RESPONSE_MS = 1_000;
|
|
11
|
-
class BodySupervisor {
|
|
12
|
-
paths;
|
|
13
|
-
bodySequence;
|
|
14
|
-
leash;
|
|
15
|
-
signal;
|
|
16
|
-
controller = new AbortController();
|
|
17
|
-
finished = new AbortController();
|
|
18
|
-
observer;
|
|
19
|
-
waiter;
|
|
20
|
-
observation;
|
|
21
|
-
stopping;
|
|
22
|
-
constructor(paths, bodySequence, leash, observation) {
|
|
23
|
-
this.paths = paths;
|
|
24
|
-
this.bodySequence = bodySequence;
|
|
25
|
-
this.leash = leash;
|
|
26
|
-
this.signal = this.controller.signal;
|
|
27
|
-
this.observation = observation;
|
|
28
|
-
this.observer = this.observe();
|
|
29
|
-
}
|
|
30
|
-
static async open(paths, bodySequence, leash) {
|
|
31
|
-
return new BodySupervisor(paths, bodySequence, leash, await readHeart(paths));
|
|
32
|
-
}
|
|
33
|
-
get reason() { return this.stopping; }
|
|
34
|
-
current() { return this.observation; }
|
|
35
|
-
async recordHung(diagnostic, at) {
|
|
36
|
-
await this.leash.recordBodyHung(this.paths, { sequence: this.bodySequence, diagnostic, at });
|
|
37
|
-
}
|
|
38
|
-
async refresh() {
|
|
39
|
-
this.publish(await readHeart(this.paths));
|
|
40
|
-
return this.observation;
|
|
41
|
-
}
|
|
42
|
-
next(after) {
|
|
43
|
-
if (this.observation !== after)
|
|
44
|
-
return Promise.resolve(this.observation);
|
|
45
|
-
return new Promise((resolve) => { this.waiter = resolve; });
|
|
46
|
-
}
|
|
47
|
-
cancel(reason) {
|
|
48
|
-
if (this.stopping !== undefined)
|
|
49
|
-
return;
|
|
50
|
-
this.stopping = reason;
|
|
51
|
-
this.controller.abort(new Error(reason === "control"
|
|
52
|
-
? "Akuma Body interrupted by durable control"
|
|
53
|
-
: "Akuma Heart disappeared"));
|
|
54
|
-
this.waiter?.(this.observation);
|
|
55
|
-
this.waiter = undefined;
|
|
56
|
-
}
|
|
57
|
-
async close() {
|
|
58
|
-
this.finished.abort();
|
|
59
|
-
await this.observer;
|
|
60
|
-
this.waiter = undefined;
|
|
61
|
-
}
|
|
62
|
-
publish(snapshot) {
|
|
63
|
-
this.observation = snapshot;
|
|
64
|
-
this.waiter?.(snapshot);
|
|
65
|
-
this.waiter = undefined;
|
|
66
|
-
}
|
|
67
|
-
async observe() {
|
|
68
|
-
for (;;) {
|
|
69
|
-
const snapshot = this.observation;
|
|
70
|
-
if (!await heartExists(this.paths)) {
|
|
71
|
-
this.cancel("heart-gone");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (bodyControlRequested(snapshot, this.bodySequence)) {
|
|
75
|
-
this.cancel("control");
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
await abortableDelay(BODY_CONTROL_OBSERVATION_MS, this.finished.signal);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
await this.refresh();
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
if (await heartExists(this.paths))
|
|
89
|
-
throw error;
|
|
90
|
-
this.cancel("heart-gone");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function serializeEffects() {
|
|
97
|
-
let tail = Promise.resolve();
|
|
98
|
-
let failed = false;
|
|
99
|
-
let failure;
|
|
100
|
-
const serialize = async (effect) => {
|
|
101
|
-
const result = tail.then(effect);
|
|
102
|
-
tail = result.then(() => undefined, (error) => {
|
|
103
|
-
if (!failed)
|
|
104
|
-
failure = error;
|
|
105
|
-
failed = true;
|
|
106
|
-
});
|
|
107
|
-
return await result;
|
|
108
|
-
};
|
|
109
|
-
serialize.drain = async () => {
|
|
110
|
-
await tail;
|
|
111
|
-
if (failed)
|
|
112
|
-
throw failure;
|
|
113
|
-
};
|
|
114
|
-
return serialize;
|
|
115
|
-
}
|
|
9
|
+
export { CONTROL_RESPONSE_MS } from "./body-turn.js";
|
|
116
10
|
function missing(error) {
|
|
117
11
|
return isHeartAbsent(error) || error.code === "ENOENT";
|
|
118
12
|
}
|
|
119
|
-
async function writeProviderEvent(input, active, event) {
|
|
120
|
-
if (input.supervisor.signal.aborted)
|
|
121
|
-
return;
|
|
122
|
-
const at = input.now();
|
|
123
|
-
if (event.type === "session") {
|
|
124
|
-
await recordSession(input.paths, {
|
|
125
|
-
provider: input.soul.provider.name,
|
|
126
|
-
coordinate: event.coordinate,
|
|
127
|
-
cwd: active.cwd,
|
|
128
|
-
options: active.options,
|
|
129
|
-
admittedAt: at,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
await appendActivity(input.paths, {
|
|
133
|
-
turnSequence: active.turnSequence,
|
|
134
|
-
event: encodeAgentEvent(event),
|
|
135
|
-
at,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
function bodyControlRequested(snapshot, bodySequence) {
|
|
139
|
-
return snapshot.stop?.bodySequence === bodySequence
|
|
140
|
-
|| snapshot.pause?.bodySequence === bodySequence;
|
|
141
|
-
}
|
|
142
13
|
async function putDownIfControlled(paths, supervisor, bodySequence, now) {
|
|
143
14
|
if (supervisor.reason === "control" && await heartExists(paths)) {
|
|
144
15
|
await breakBody(paths, { sequence: bodySequence, end: "put-down", at: now() });
|
|
145
16
|
}
|
|
146
17
|
}
|
|
147
|
-
async function turnRecipe(paths, soul) {
|
|
148
|
-
const latest = (await readHeart(paths)).latestSession;
|
|
149
|
-
const admitted = latest?.provider === soul.provider.name ? latest : undefined;
|
|
150
|
-
const session = admitted?.coordinate;
|
|
151
|
-
return {
|
|
152
|
-
cwd: admitted?.cwd ?? soul.cwd,
|
|
153
|
-
options: admitted?.options ?? soul.options,
|
|
154
|
-
...(session === undefined ? {} : { session }),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
18
|
async function takeLeash(paths) {
|
|
158
19
|
for (;;) {
|
|
159
20
|
try {
|
|
@@ -169,267 +30,6 @@ async function takeLeash(paths) {
|
|
|
169
30
|
await abortableDelay(LEASH_RETRY_MS);
|
|
170
31
|
}
|
|
171
32
|
}
|
|
172
|
-
async function startTurnDrive(input) {
|
|
173
|
-
const { cwd, options, session } = await turnRecipe(input.paths, input.soul);
|
|
174
|
-
if (session !== undefined && input.adapter.resume === undefined)
|
|
175
|
-
return { kind: "resume-unsupported" };
|
|
176
|
-
const turn = await beginTurn(input.paths, {
|
|
177
|
-
bodySequence: input.bodySequence,
|
|
178
|
-
startedAt: input.now(),
|
|
179
|
-
...(input.call === undefined ? {} : { call: input.call }),
|
|
180
|
-
});
|
|
181
|
-
const requests = await BodyRequestPump.open({
|
|
182
|
-
paths: input.paths,
|
|
183
|
-
parent: input.soul,
|
|
184
|
-
bodySequence: input.bodySequence,
|
|
185
|
-
now: input.now,
|
|
186
|
-
spawn: input.runtimeSpawn,
|
|
187
|
-
...(input.upstream === undefined ? {} : { upstream: input.upstream }),
|
|
188
|
-
signal: input.supervisor.signal,
|
|
189
|
-
});
|
|
190
|
-
const driveInput = {
|
|
191
|
-
body: input.body,
|
|
192
|
-
launchTells: input.launchTells.map((tell) => ({ id: tell.id, text: tell.body })),
|
|
193
|
-
cwd,
|
|
194
|
-
options,
|
|
195
|
-
signal: input.supervisor.signal,
|
|
196
|
-
requests: { dir: requests.directory },
|
|
197
|
-
};
|
|
198
|
-
const setup = session === undefined
|
|
199
|
-
? input.adapter.start({ ...driveInput, session: { kind: "fresh" } })
|
|
200
|
-
: input.adapter.resume({ ...driveInput, session: { kind: "resume", coordinate: session } });
|
|
201
|
-
try {
|
|
202
|
-
const selected = await setup;
|
|
203
|
-
if (input.supervisor.signal.aborted) {
|
|
204
|
-
await retireProviderCustody(input, turn.sequence, selected);
|
|
205
|
-
await requests.close();
|
|
206
|
-
return { kind: "stopped" };
|
|
207
|
-
}
|
|
208
|
-
if (input.launchTells.length > 0) {
|
|
209
|
-
await recordTellDeliveries(input.paths, input.launchTells.map((tell) => ({
|
|
210
|
-
tellId: tell.id,
|
|
211
|
-
route: "launch",
|
|
212
|
-
turnSequence: turn.sequence,
|
|
213
|
-
fence: selected.admission.fence,
|
|
214
|
-
deliveredAt: input.now(),
|
|
215
|
-
})));
|
|
216
|
-
}
|
|
217
|
-
void selected.completion.then(() => requests.stopAdmission());
|
|
218
|
-
return { turnSequence: turn.sequence, drive: selected, requests, cwd, options,
|
|
219
|
-
...(session === undefined ? {} : { resume: session }) };
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
await requests.close();
|
|
223
|
-
if (input.supervisor.signal.aborted) {
|
|
224
|
-
if (input.supervisor.reason === "heart-gone")
|
|
225
|
-
throw error;
|
|
226
|
-
return { kind: "stopped" };
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
229
|
-
kind: "failed",
|
|
230
|
-
turnSequence: turn.sequence,
|
|
231
|
-
diagnostic: error instanceof Error ? error.message : String(error),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async function retireProviderCustody(input, turnSequence, drive) {
|
|
236
|
-
const outcome = await Promise.race([
|
|
237
|
-
drive.abort().then(() => ({ kind: "retired" }), (error) => ({ kind: "held", error })),
|
|
238
|
-
abortableDelay(CONTROL_RESPONSE_MS).then(() => ({
|
|
239
|
-
kind: "held",
|
|
240
|
-
error: new Error(`provider custody remained live after ${CONTROL_RESPONSE_MS}ms`),
|
|
241
|
-
})),
|
|
242
|
-
]);
|
|
243
|
-
if (outcome.kind === "retired")
|
|
244
|
-
return;
|
|
245
|
-
if (await heartExists(input.paths)) {
|
|
246
|
-
const diagnostic = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
247
|
-
const at = input.now();
|
|
248
|
-
try {
|
|
249
|
-
await input.supervisor.recordHung(diagnostic, at);
|
|
250
|
-
await appendActivity(input.paths, {
|
|
251
|
-
turnSequence,
|
|
252
|
-
event: encodeAgentEvent({ type: "note", text: `Provider custody was not retired: ${diagnostic}` }),
|
|
253
|
-
at,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
catch { /* undisposed provider custody still owns the leash */ }
|
|
257
|
-
}
|
|
258
|
-
for (;;)
|
|
259
|
-
await abortableDelay(60_000);
|
|
260
|
-
}
|
|
261
|
-
function pumpReceipts(writers) {
|
|
262
|
-
const { input, turnSequence, drive, writeWitness, mayWrite } = writers;
|
|
263
|
-
return (async () => {
|
|
264
|
-
if (drive.receipts === undefined)
|
|
265
|
-
return;
|
|
266
|
-
for await (const receipt of drive.receipts) {
|
|
267
|
-
if (!mayWrite())
|
|
268
|
-
return;
|
|
269
|
-
await writeWitness(async () => {
|
|
270
|
-
if (!mayWrite())
|
|
271
|
-
return;
|
|
272
|
-
await recordTellReceipt(input.paths, receipt.evidence === "exact"
|
|
273
|
-
? { ...receipt, receivedAt: input.now() }
|
|
274
|
-
: { ...receipt, turnSequence, receivedAt: input.now() });
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
})();
|
|
278
|
-
}
|
|
279
|
-
function observeReceiptFailure(receiptPump) {
|
|
280
|
-
return receiptPump.then(() => new Promise(() => { }), (error) => Promise.reject(error));
|
|
281
|
-
}
|
|
282
|
-
async function submitPendingLiveTells(writers, pending, attempted, tellLive) {
|
|
283
|
-
const { input, turnSequence, drive, writeWitness, mayWrite } = writers;
|
|
284
|
-
for (const tell of pending) {
|
|
285
|
-
if (attempted.has(tell.id))
|
|
286
|
-
continue;
|
|
287
|
-
attempted.add(tell.id);
|
|
288
|
-
const outcome = await writeWitness(async () => {
|
|
289
|
-
const submission = await tellLive({ id: tell.id, text: tell.body });
|
|
290
|
-
if (!mayWrite())
|
|
291
|
-
return "turn-ended";
|
|
292
|
-
if (submission.kind === "turn-ended")
|
|
293
|
-
return "turn-ended";
|
|
294
|
-
await recordTellDeliveries(input.paths, [{
|
|
295
|
-
tellId: tell.id,
|
|
296
|
-
route: "live",
|
|
297
|
-
turnSequence,
|
|
298
|
-
fence: submission.fence,
|
|
299
|
-
receipt: drive.receipts === undefined ? "unavailable" : "required",
|
|
300
|
-
deliveredAt: input.now(),
|
|
301
|
-
}]);
|
|
302
|
-
return "accepted";
|
|
303
|
-
});
|
|
304
|
-
if (outcome === "turn-ended")
|
|
305
|
-
return "turn-ended";
|
|
306
|
-
}
|
|
307
|
-
return "live";
|
|
308
|
-
}
|
|
309
|
-
async function stopActiveDrive(input, active) {
|
|
310
|
-
await retireProviderCustody(input, active.turnSequence, active.drive);
|
|
311
|
-
await active.requests.close();
|
|
312
|
-
}
|
|
313
|
-
function hasUnattemptedTell(liveTells, tellPump, pending, attempted) {
|
|
314
|
-
return liveTells && tellPump === null && pending.some((tell) => !attempted.has(tell.id));
|
|
315
|
-
}
|
|
316
|
-
function persistProviderEvent(_input, _active, event) {
|
|
317
|
-
return event.type === "session" ? event.coordinate : undefined;
|
|
318
|
-
}
|
|
319
|
-
async function settleCompletion(result, session, requests) {
|
|
320
|
-
await requests.close();
|
|
321
|
-
return result.kind === "answered" ? { ...result, ...(session === undefined ? {} : { session }) } : result;
|
|
322
|
-
}
|
|
323
|
-
async function consumeTurnDrive(input, active) {
|
|
324
|
-
const { turnSequence, drive, requests, resume } = active;
|
|
325
|
-
let heart = input.supervisor.current();
|
|
326
|
-
let turnSession = resume;
|
|
327
|
-
const attempted = new Set(input.launchTells.map((tell) => tell.id));
|
|
328
|
-
const writeWitness = serializeEffects();
|
|
329
|
-
let writesOpen = true;
|
|
330
|
-
const mayWrite = () => writesOpen && !input.supervisor.signal.aborted;
|
|
331
|
-
const writers = { input, turnSequence, drive, writeWitness, mayWrite };
|
|
332
|
-
const receiptPump = pumpReceipts(writers);
|
|
333
|
-
const receiptFailure = observeReceiptFailure(receiptPump);
|
|
334
|
-
const iterator = drive.events[Symbol.asyncIterator]();
|
|
335
|
-
let pending = iterator.next();
|
|
336
|
-
let liveTells = true;
|
|
337
|
-
let tellPump = null;
|
|
338
|
-
let tellObservation = null;
|
|
339
|
-
try {
|
|
340
|
-
for (;;) {
|
|
341
|
-
if (input.supervisor.signal.aborted) {
|
|
342
|
-
writesOpen = false;
|
|
343
|
-
await stopActiveDrive(input, active);
|
|
344
|
-
return { kind: "stopped" };
|
|
345
|
-
}
|
|
346
|
-
if (hasUnattemptedTell(liveTells, tellPump, heart.pending, attempted)) {
|
|
347
|
-
if (drive.tell === undefined) {
|
|
348
|
-
writesOpen = false;
|
|
349
|
-
await stopActiveDrive(input, active);
|
|
350
|
-
return { kind: "handoff" };
|
|
351
|
-
}
|
|
352
|
-
tellPump = submitPendingLiveTells(writers, heart.pending, attempted, drive.tell);
|
|
353
|
-
tellObservation = tellPump.then((result) => ({ kind: "tell", result }));
|
|
354
|
-
}
|
|
355
|
-
const next = await Promise.race([
|
|
356
|
-
pending.then((event) => ({ kind: "event", event })),
|
|
357
|
-
input.supervisor.next(heart).then((observation) => ({ kind: "heart", observation })),
|
|
358
|
-
...(tellObservation === null ? [] : [tellObservation]),
|
|
359
|
-
receiptFailure,
|
|
360
|
-
requests.failure,
|
|
361
|
-
]);
|
|
362
|
-
if (next.kind === "event" && !next.event.done) {
|
|
363
|
-
await writeProviderEvent(input, active, next.event.value);
|
|
364
|
-
}
|
|
365
|
-
if (next.kind === "heart") {
|
|
366
|
-
heart = next.observation;
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
if (next.kind === "tell") {
|
|
370
|
-
tellPump = null;
|
|
371
|
-
tellObservation = null;
|
|
372
|
-
liveTells = next.result === "live";
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (next.event.done)
|
|
376
|
-
break;
|
|
377
|
-
turnSession = persistProviderEvent(input, active, next.event.value) ?? turnSession;
|
|
378
|
-
pending = iterator.next();
|
|
379
|
-
}
|
|
380
|
-
const result = await drive.completion;
|
|
381
|
-
if (tellPump !== null)
|
|
382
|
-
await tellPump;
|
|
383
|
-
await writeWitness.drain();
|
|
384
|
-
writesOpen = false;
|
|
385
|
-
return await settleCompletion(result, turnSession, requests);
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
writesOpen = false;
|
|
389
|
-
if (input.supervisor.signal.aborted) {
|
|
390
|
-
await stopActiveDrive(input, active);
|
|
391
|
-
return { kind: "stopped" };
|
|
392
|
-
}
|
|
393
|
-
await retireProviderCustody(input, turnSequence, drive);
|
|
394
|
-
try {
|
|
395
|
-
await requests.close();
|
|
396
|
-
}
|
|
397
|
-
catch { /* preserve the first pump failure */ }
|
|
398
|
-
return { kind: "failed", diagnostic: error instanceof Error ? error.message : String(error) };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
async function driveTurn(input) {
|
|
402
|
-
let active;
|
|
403
|
-
try {
|
|
404
|
-
const started = await startTurnDrive(input);
|
|
405
|
-
if ("kind" in started)
|
|
406
|
-
return started;
|
|
407
|
-
active = started;
|
|
408
|
-
}
|
|
409
|
-
catch (error) {
|
|
410
|
-
if (!await heartExists(input.paths)) {
|
|
411
|
-
input.supervisor.cancel("heart-gone");
|
|
412
|
-
return { kind: "stopped" };
|
|
413
|
-
}
|
|
414
|
-
throw error;
|
|
415
|
-
}
|
|
416
|
-
try {
|
|
417
|
-
const result = await consumeTurnDrive(input, active);
|
|
418
|
-
return result.kind === "stopped" || result.kind === "handoff"
|
|
419
|
-
? result
|
|
420
|
-
: { ...result, turnSequence: active.turnSequence };
|
|
421
|
-
}
|
|
422
|
-
catch (error) {
|
|
423
|
-
if (await heartExists(input.paths)) {
|
|
424
|
-
await active.requests.close();
|
|
425
|
-
return { kind: "failed", diagnostic: error instanceof Error ? error.message : String(error),
|
|
426
|
-
turnSequence: active.turnSequence };
|
|
427
|
-
}
|
|
428
|
-
input.supervisor.cancel("heart-gone");
|
|
429
|
-
await stopActiveDrive(input, active);
|
|
430
|
-
return { kind: "stopped" };
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
33
|
async function bornSoul(launch, leash, now) {
|
|
434
34
|
const before = await readHeart(launch.paths);
|
|
435
35
|
if (before.soul !== null)
|
|
@@ -539,6 +139,8 @@ async function runBodyTurns(input) {
|
|
|
539
139
|
...(runtime.upstream === undefined ? {} : { upstream: runtime.upstream }),
|
|
540
140
|
now: runtime.now,
|
|
541
141
|
});
|
|
142
|
+
if (result.kind === "hung")
|
|
143
|
+
return;
|
|
542
144
|
if (result.kind === "stopped") {
|
|
543
145
|
await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now);
|
|
544
146
|
return;
|
|
@@ -564,7 +166,7 @@ export async function driveAkumaBody(launch, adapter, runtime = defaultRuntime()
|
|
|
564
166
|
const soul = await bornSoul(launch, leash, runtime.now());
|
|
565
167
|
if (soul === null)
|
|
566
168
|
return;
|
|
567
|
-
const selected = adapter ?? resolveProviderExecution(soul.provider).adapter;
|
|
169
|
+
const selected = adapter ?? (await resolveProviderExecution(soul.provider)).adapter;
|
|
568
170
|
if (!await prepareBodyStart(launch.paths, leash))
|
|
569
171
|
return;
|
|
570
172
|
const body = await leash.recordBody(launch.paths, { leashTakenAt: runtime.now() });
|
|
@@ -607,23 +209,10 @@ export async function spawnAkumaBody(launch) {
|
|
|
607
209
|
});
|
|
608
210
|
owned.release();
|
|
609
211
|
}
|
|
610
|
-
export async function runAkumaBody(
|
|
611
|
-
const encoded = process.argv[2];
|
|
612
|
-
if (encoded === undefined)
|
|
613
|
-
throw new TypeError("Akuma body launch payload is missing");
|
|
614
|
-
const launch = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
615
|
-
const mappedHome = process.env.KEIYAKU_HOME?.trim();
|
|
616
|
-
const mappedGitPath = process.env.KEIYAKU_GIT_PATH;
|
|
617
|
-
if (mappedGitPath !== undefined && mappedGitPath.trim().length === 0) {
|
|
618
|
-
throw new TypeError("KEIYAKU_GIT_PATH requires a nonblank value");
|
|
619
|
-
}
|
|
620
|
-
const configuration = {
|
|
621
|
-
...(mappedHome === undefined || mappedHome.length === 0 ? {} : { home: mappedHome }),
|
|
622
|
-
...(mappedGitPath === undefined ? {} : { gitPath: mappedGitPath }),
|
|
623
|
-
};
|
|
212
|
+
export async function runAkumaBody(launch, upstream) {
|
|
624
213
|
await driveAkumaBody(launch, undefined, {
|
|
625
214
|
now: () => new Date().toISOString(),
|
|
626
215
|
spawnChild: spawnAkumaBody,
|
|
627
|
-
upstream
|
|
216
|
+
upstream,
|
|
628
217
|
});
|
|
629
218
|
}
|
|
@@ -159,6 +159,7 @@ export type RequestInput = Readonly<{
|
|
|
159
159
|
contractId: string;
|
|
160
160
|
message?: string;
|
|
161
161
|
includeDirty: boolean;
|
|
162
|
+
materializeConflict: boolean;
|
|
162
163
|
}> | Readonly<{
|
|
163
164
|
id: string;
|
|
164
165
|
action: "contract.review";
|
|
@@ -249,6 +250,7 @@ export type HeartSnapshot = Readonly<{
|
|
|
249
250
|
latestKill: KillFact | null;
|
|
250
251
|
stop: StopFact | null;
|
|
251
252
|
pause: PauseFact | null;
|
|
253
|
+
lastActivityAt: string | null;
|
|
252
254
|
}>;
|
|
253
255
|
export declare function life(input: Readonly<{
|
|
254
256
|
leash: LeashProbe;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export function life(input) {
|
|
2
|
+
if (input.body?.hung !== undefined)
|
|
3
|
+
return "hung";
|
|
2
4
|
if (input.leash === "held")
|
|
3
|
-
return
|
|
5
|
+
return "running";
|
|
4
6
|
if (input.body === null || input.body.end === undefined)
|
|
5
7
|
return "untidy";
|
|
6
8
|
if (input.kill?.bodySequence === input.body.sequence)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answeredTurnFact, endBodyFact, finishBodyFact, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertPauseControl, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, latestKillFact, latestSessionFact, lastAnsweredTurnFact, pauseFact, sessionFactForCoordinate, stopFact, } from "./rows.js";
|
|
2
2
|
import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingTellFacts, tellFact, tellIdsForFence, } from "./tells.js";
|
|
3
|
-
import { activityFactSlice, pruneActivityFacts, } from "./timeline.js";
|
|
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
6
|
export { HeartAbsentError, HeldAkumaLeash, initializeHeart, isHeartAbsent, probeLeash } from "./storage.js";
|
|
@@ -172,11 +172,21 @@ export async function readHeart(paths) {
|
|
|
172
172
|
latestKill: latestKillFact(heart),
|
|
173
173
|
stop: stopFact(heart),
|
|
174
174
|
pause: pauseFact(heart),
|
|
175
|
+
lastActivityAt: readLastActivityAt(heart),
|
|
175
176
|
})));
|
|
176
177
|
}
|
|
177
178
|
catch (error) {
|
|
178
179
|
if (isHeartAbsent(error)) {
|
|
179
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
soul: null,
|
|
182
|
+
latestBody: null,
|
|
183
|
+
latestSession: null,
|
|
184
|
+
pending: [],
|
|
185
|
+
latestKill: null,
|
|
186
|
+
stop: null,
|
|
187
|
+
pause: null,
|
|
188
|
+
lastActivityAt: null,
|
|
189
|
+
};
|
|
180
190
|
}
|
|
181
191
|
throw error;
|
|
182
192
|
}
|
|
@@ -96,6 +96,9 @@ export function sealFact(database) {
|
|
|
96
96
|
return row === undefined ? null : { evidence: row.evidence, at: row.at };
|
|
97
97
|
}
|
|
98
98
|
export function insertBodyFact(database, input) {
|
|
99
|
+
if (latestBodyFact(database)?.hung !== undefined) {
|
|
100
|
+
throw new Error("Akuma successor is permanently gated by hung custody");
|
|
101
|
+
}
|
|
99
102
|
const result = database.prepare("INSERT INTO bodies(leash_taken_at) VALUES (?)").run(input.leashTakenAt);
|
|
100
103
|
return Number(result.lastInsertRowid);
|
|
101
104
|
}
|
|
@@ -151,7 +154,7 @@ export function endBodyFact(database, input) {
|
|
|
151
154
|
}
|
|
152
155
|
export function markBodyHung(database, input) {
|
|
153
156
|
const result = database.prepare(`UPDATE bodies SET hung_diagnostic = ?, hung_at = ?
|
|
154
|
-
WHERE sequence = ? AND
|
|
157
|
+
WHERE sequence = ? AND hung_diagnostic IS NULL`)
|
|
155
158
|
.run(input.diagnostic, input.at, input.sequence);
|
|
156
159
|
if (result.changes !== 1)
|
|
157
160
|
throw new Error(`Akuma Body ${input.sequence} cannot record hung custody`);
|
|
@@ -1,5 +1,5 @@
|
|
|
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 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 =
|
|
4
|
+
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
5
|
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 =
|
|
1
|
+
const HEART_SCHEMA_VERSION = 20;
|
|
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();
|
|
@@ -28,7 +28,8 @@ export const HEART_SCHEMA = `
|
|
|
28
28
|
hung_at TEXT,
|
|
29
29
|
end TEXT CHECK (end IN ('exited', 'broke-off', 'put-down')),
|
|
30
30
|
ended_at TEXT,
|
|
31
|
-
CHECK ((hung_diagnostic IS NULL AND hung_at IS NULL)
|
|
31
|
+
CHECK ((hung_diagnostic IS NULL AND hung_at IS NULL)
|
|
32
|
+
OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL))
|
|
32
33
|
) STRICT;
|
|
33
34
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
34
35
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -7,5 +7,6 @@ export type ActivityFactSlice = Readonly<{
|
|
|
7
7
|
lowestRetained: number | null;
|
|
8
8
|
highest: number | null;
|
|
9
9
|
}>;
|
|
10
|
+
export declare function lastActivityAt(database: DatabaseSync): string | null;
|
|
10
11
|
export declare function pruneActivityFacts(database: DatabaseSync, limit: number): void;
|
|
11
12
|
export declare function activityFactSlice(database: DatabaseSync): ActivityFactSlice;
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { decodeActivityRow, decodeCallRow, decodeTurnRow, } from "./rows.js";
|
|
2
2
|
import { decodeTellAtSequence, pendingTellProtectionSql, pendingTellSequencesSql } from "./tells.js";
|
|
3
|
+
export function lastActivityAt(database) {
|
|
4
|
+
const row = database.prepare(`SELECT CASE timeline.kind
|
|
5
|
+
WHEN 'turn-start' THEN started_turn.started_at
|
|
6
|
+
WHEN 'call' THEN calls.at
|
|
7
|
+
WHEN 'activity' THEN activity.at
|
|
8
|
+
WHEN 'tell' THEN tells.recorded_at
|
|
9
|
+
WHEN 'turn-end' THEN ended_turn.completed_at
|
|
10
|
+
END AS at
|
|
11
|
+
FROM timeline
|
|
12
|
+
LEFT JOIN turns AS started_turn ON timeline.kind = 'turn-start' AND started_turn.sequence = timeline.sequence
|
|
13
|
+
LEFT JOIN calls ON timeline.kind = 'call' AND calls.sequence = timeline.sequence
|
|
14
|
+
LEFT JOIN activity ON timeline.kind = 'activity' AND activity.sequence = timeline.sequence
|
|
15
|
+
LEFT JOIN tells ON timeline.kind = 'tell' AND tells.sequence = timeline.sequence
|
|
16
|
+
LEFT JOIN turns AS ended_turn ON timeline.kind = 'turn-end' AND ended_turn.end_sequence = timeline.sequence
|
|
17
|
+
ORDER BY timeline.sequence DESC LIMIT 1`).get();
|
|
18
|
+
if (row === undefined || row.at === null)
|
|
19
|
+
return null;
|
|
20
|
+
return row.at;
|
|
21
|
+
}
|
|
3
22
|
export function pruneActivityFacts(database, limit) {
|
|
4
23
|
const count = database.prepare("SELECT COUNT(*) AS count FROM timeline").get();
|
|
5
24
|
if (count.count <= limit + 500)
|