@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.
Files changed (153) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
  2. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +1 -1
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +15 -7
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +5 -3
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +10 -4
  8. package/build/src/akuma/akuma.d.ts +2 -0
  9. package/build/src/akuma/akuma.js +26 -3
  10. package/build/src/akuma/archetype.d.ts +3 -1
  11. package/build/src/akuma/archetype.js +11 -6
  12. package/build/src/akuma/body-turn.d.ts +64 -0
  13. package/build/src/akuma/body-turn.js +449 -0
  14. package/build/src/akuma/body.d.ts +3 -7
  15. package/build/src/akuma/body.js +9 -420
  16. package/build/src/akuma/heart/facts.d.ts +2 -0
  17. package/build/src/akuma/heart/facts.js +3 -1
  18. package/build/src/akuma/heart/index.js +12 -2
  19. package/build/src/akuma/heart/rows.js +4 -1
  20. package/build/src/akuma/heart/schema.d.ts +1 -1
  21. package/build/src/akuma/heart/schema.js +3 -2
  22. package/build/src/akuma/heart/timeline.d.ts +1 -0
  23. package/build/src/akuma/heart/timeline.js +19 -0
  24. package/build/src/akuma/nuke.d.ts +2 -0
  25. package/build/src/akuma/nuke.js +152 -0
  26. package/build/src/akuma/provider.d.ts +3 -1
  27. package/build/src/akuma/providers/acp/config.d.ts +8 -0
  28. package/build/src/akuma/providers/acp/config.js +37 -0
  29. package/build/src/akuma/providers/acp/core.js +6 -0
  30. package/build/src/akuma/providers/acp/index.d.ts +1 -8
  31. package/build/src/akuma/providers/acp/index.js +2 -37
  32. package/build/src/akuma/providers/claude/index.js +5 -0
  33. package/build/src/akuma/providers/codex-app-server/index.js +5 -0
  34. package/build/src/akuma/providers/grok-build/index.js +1 -1
  35. package/build/src/akuma/providers/index.d.ts +3 -2
  36. package/build/src/akuma/providers/index.js +36 -36
  37. package/build/src/akuma/providers/opencode-sdk/index.js +6 -0
  38. package/build/src/akuma/providers/opencode-sdk/session.d.ts +1 -1
  39. package/build/src/akuma/providers/opencode-sdk/session.js +1 -1
  40. package/build/src/akuma/providers/pi/index.d.ts +1 -1
  41. package/build/src/akuma/providers/pi/index.js +27 -11
  42. package/build/src/akuma/request-serve.d.ts +1 -0
  43. package/build/src/akuma/request-serve.js +31 -31
  44. package/build/src/akuma/request-wire.d.ts +3 -2
  45. package/build/src/akuma/request-wire.js +5 -2
  46. package/build/src/akuma-body.js +15 -1
  47. package/build/src/alias/index.d.ts +1 -0
  48. package/build/src/alias/index.js +34 -1
  49. package/build/src/cli/accepted.js +4 -0
  50. package/build/src/cli/commands/akuma-invoke.js +1 -0
  51. package/build/src/cli/commands/akuma.d.ts +1 -0
  52. package/build/src/cli/commands/akuma.js +11 -8
  53. package/build/src/cli/commands/contract.d.ts +25 -14
  54. package/build/src/cli/commands/contract.js +23 -17
  55. package/build/src/cli/commands/install.d.ts +2 -2
  56. package/build/src/cli/commands/install.js +6 -6
  57. package/build/src/cli/commands/nuke.d.ts +10 -0
  58. package/build/src/cli/commands/nuke.js +20 -0
  59. package/build/src/cli/commands/task-invoke.d.ts +15 -0
  60. package/build/src/cli/commands/task-invoke.js +57 -26
  61. package/build/src/cli/commands/task-query.d.ts +2 -2
  62. package/build/src/cli/commands/task-query.js +1 -2
  63. package/build/src/cli/commands/task.js +22 -22
  64. package/build/src/cli/coordinates.d.ts +5 -2
  65. package/build/src/cli/coordinates.js +5 -52
  66. package/build/src/cli/invoke.d.ts +5 -4
  67. package/build/src/cli/invoke.js +89 -59
  68. package/build/src/cli/main.d.ts +0 -2
  69. package/build/src/cli/main.js +4 -140
  70. package/build/src/cli/parse.d.ts +15 -2
  71. package/build/src/cli/parse.js +73 -8
  72. package/build/src/cli/render/akuma.js +55 -40
  73. package/build/src/cli/render/contract.js +49 -48
  74. package/build/src/cli/render/kanshi.js +15 -5
  75. package/build/src/cli/render/nuke.d.ts +3 -0
  76. package/build/src/cli/render/nuke.js +8 -0
  77. package/build/src/cli/render/refusal.d.ts +2 -1
  78. package/build/src/cli/render/refusal.js +31 -0
  79. package/build/src/cli/render/text.js +6 -1
  80. package/build/src/cli/result.d.ts +21 -2
  81. package/build/src/cli/runtime.d.ts +2 -0
  82. package/build/src/cli/runtime.js +140 -0
  83. package/build/src/cli/usage.d.ts +0 -2
  84. package/build/src/cli/usage.js +0 -4
  85. package/build/src/contract-worktree.d.ts +28 -12
  86. package/build/src/contract-worktree.js +129 -63
  87. package/build/src/git/hooks.d.ts +2 -0
  88. package/build/src/git/hooks.js +34 -1
  89. package/build/src/git/integration.d.ts +4 -0
  90. package/build/src/git/integration.js +38 -0
  91. package/build/src/git/nuke.d.ts +2 -0
  92. package/build/src/git/nuke.js +97 -0
  93. package/build/src/git/process.d.ts +2 -0
  94. package/build/src/git/reconcile.d.ts +2 -0
  95. package/build/src/git/reconcile.js +33 -2
  96. package/build/src/git/repository.js +4 -1
  97. package/build/src/git/target-placement.d.ts +2 -1
  98. package/build/src/git/target-placement.js +6 -7
  99. package/build/src/git/tender.d.ts +1 -0
  100. package/build/src/git/tender.js +2 -2
  101. package/build/src/git/workspace.d.ts +3 -0
  102. package/build/src/kanshi/read.js +5 -1
  103. package/build/src/library/akuma-creation.d.ts +1 -0
  104. package/build/src/library/akuma-creation.js +8 -1
  105. package/build/src/library/audit.d.ts +1 -0
  106. package/build/src/library/audit.js +1 -0
  107. package/build/src/library/contract.d.ts +6 -4
  108. package/build/src/library/contract.js +56 -34
  109. package/build/src/library/delivery.d.ts +3 -1
  110. package/build/src/library/keiyaku.d.ts +5 -1
  111. package/build/src/library/keiyaku.js +2 -0
  112. package/build/src/library/mutation.d.ts +1 -0
  113. package/build/src/library/mutation.js +3 -0
  114. package/build/src/library/nuke.d.ts +14 -0
  115. package/build/src/library/nuke.js +54 -0
  116. package/build/src/library/reconcile.js +5 -2
  117. package/build/src/library/refusal.d.ts +10 -1
  118. package/build/src/library/repo.d.ts +1 -0
  119. package/build/src/library/repo.js +2 -0
  120. package/build/src/protocol/audit.js +12 -5
  121. package/build/src/protocol/completion.d.ts +11 -1
  122. package/build/src/protocol/completion.js +68 -15
  123. package/build/src/protocol/deliver.d.ts +14 -2
  124. package/build/src/protocol/deliver.js +107 -6
  125. package/build/src/protocol/intent.d.ts +1 -0
  126. package/build/src/protocol/intent.js +7 -1
  127. package/build/src/protocol/operations.d.ts +30 -3
  128. package/build/src/protocol/operations.js +2 -2
  129. package/build/src/protocol/placement.d.ts +9 -1
  130. package/build/src/protocol/placement.js +5 -4
  131. package/build/src/protocol/read/status.d.ts +16 -4
  132. package/build/src/protocol/read/status.js +54 -12
  133. package/build/src/protocol/review.d.ts +1 -0
  134. package/build/src/protocol/review.js +8 -0
  135. package/build/src/runtime/proc/run.js +9 -5
  136. package/build/src/settlement/fence.d.ts +2 -0
  137. package/build/src/settlement/fence.js +24 -0
  138. package/build/src/settlement/settle.js +1 -2
  139. package/build/src/task/compose.d.ts +1 -1
  140. package/build/src/task/compose.js +2 -7
  141. package/build/src/task/context.d.ts +14 -2
  142. package/build/src/task/context.js +30 -7
  143. package/build/src/task/index.d.ts +6 -5
  144. package/build/src/task/index.js +15 -18
  145. package/build/src/task/mutation.d.ts +7 -2
  146. package/build/src/task/mutation.js +14 -6
  147. package/build/src/task/operations.d.ts +12 -6
  148. package/build/src/task/operations.js +22 -46
  149. package/build/src/task/store.d.ts +1 -0
  150. package/build/src/task/store.js +91 -1
  151. package/build/src/workspace-place.d.ts +2 -0
  152. package/build/src/workspace-place.js +27 -1
  153. package/package.json +1 -1
@@ -1,159 +1,20 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { abortableDelay } from "./abort.js";
3
- import { HeldAkumaLeash, appendActivity, beginTurn, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, recordSession, recordTellDeliveries, recordTellReceipt, } from "./heart/index.js";
4
- import { encodeAgentEvent, } from "./provider.js";
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 { BodyRequestPump, clearBodyRequestTransport, settleBodyRequests, } from "./request-serve.js";
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
- const BODY_CONTROL_OBSERVATION_MS = 100;
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(upstreamFor) {
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: upstreamFor(launch, configuration),
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 input.body?.hung === undefined ? "running" : "hung";
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 { soul: null, latestBody: null, latestSession: null, pending: [], latestKill: null, stop: null, pause: null };
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 end IS NULL AND hung_diagnostic IS NULL`)
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 = 19)\n ) STRICT;\n INSERT OR IGNORE INTO akuma_schema(singleton, version) VALUES (1, 19);\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) OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL AND end IS 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";
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 = 19;
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) OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL AND end 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)
@@ -0,0 +1,2 @@
1
+ import type { WorldRoot } from "../world.js";
2
+ export declare function stopAkuma(world: WorldRoot): Promise<() => Promise<void>>;