@astrosheep/keiyaku 4.5.6 → 4.5.7
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/opencode.js +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +1 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-babysit/SKILL.md +114 -0
- package/build/src/akuma/body.d.ts +1 -1
- package/build/src/akuma/body.js +20 -33
- package/build/src/akuma/heart/index.d.ts +2 -2
- package/build/src/akuma/heart/index.js +2 -2
- package/build/src/akuma/heart/request-authority.d.ts +1 -0
- package/build/src/akuma/heart/request-authority.js +11 -1
- package/build/src/akuma/heart/storage.d.ts +0 -1
- package/build/src/akuma/heart/storage.js +0 -54
- package/build/src/akuma/publication.js +75 -13
- package/build/src/akuma/request-lifecycle.js +9 -22
- package/build/src/akuma/request-serve.js +17 -2
- package/build/src/cli/commands/task-invoke.d.ts +4 -2
- package/build/src/cli/commands/task-invoke.js +48 -22
- package/build/src/cli/commands/task.d.ts +1 -1
- package/build/src/cli/commands/task.js +7 -3
- package/build/src/cli/coordinates.d.ts +2 -0
- package/build/src/cli/coordinates.js +18 -2
- package/build/src/cli/render/task.js +6 -5
- package/build/src/cli/square-edge.js +2 -2
- package/build/src/git/repository.d.ts +2 -0
- package/build/src/git/repository.js +34 -21
- package/build/src/git/target-placement.js +0 -6
- package/build/src/protocol/attempt.js +9 -4
- package/build/src/task/compose-language.d.ts +4 -0
- package/build/src/task/compose-language.js +7 -0
- package/build/src/task/compose-parser.d.ts +1 -0
- package/build/src/task/compose-parser.js +4 -2
- package/build/src/task/compose.d.ts +1 -0
- package/build/src/task/compose.js +2 -1
- package/build/src/task/context.d.ts +7 -1
- package/build/src/task/context.js +25 -6
- package/build/src/task/index.d.ts +6 -1
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
|
-
const skills = ["keiyaku", "keiyaku-task", "keiyaku-bind", "keiyaku-workflow", "keiyaku-akuma"]
|
|
4
|
+
const skills = ["keiyaku", "keiyaku-task", "keiyaku-bind", "keiyaku-workflow", "keiyaku-akuma", "keiyaku-babysit"]
|
|
5
5
|
.map((name) => fileURLToPath(new URL(`./skills/${name}/SKILL.md`, import.meta.url)));
|
|
6
6
|
|
|
7
7
|
export default {
|
|
@@ -14,6 +14,7 @@ read `<command> --help` before using flags.
|
|
|
14
14
|
- A Contract is delivery authority; see `keiyaku-bind` to author and bind one,
|
|
15
15
|
then `keiyaku-workflow` for the remaining lifecycle.
|
|
16
16
|
- An Akuma is a callable worker; see `keiyaku-akuma`.
|
|
17
|
+
- A long-running multi-lane goal needs coordination; see `keiyaku-babysit`.
|
|
17
18
|
|
|
18
19
|
Settings owns named gate bundles. Omitting `--gates` selects `gates.default`,
|
|
19
20
|
or freezes `["reviewed"]` when that entry is absent. `--gates <name,...>`
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keiyaku-babysit
|
|
3
|
+
description: Babysit a long-running multi-lane goal — a room of lanes that advance on their own, call for you on their schedule, and hold each other up while you are busy being productive.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# keiyaku-babysit
|
|
7
|
+
|
|
8
|
+
A goal big enough to babysit is a room full of wards: kei in delivery,
|
|
9
|
+
Akuma working, reviews pending, a caller owing a decision, an external
|
|
10
|
+
dependency due someday. Each lane advances on its own once set moving;
|
|
11
|
+
each calls for you when it chooses; a lane left waiting on you holds up
|
|
12
|
+
the lanes behind it. There is one of you. The hard part is where your
|
|
13
|
+
next ten minutes go.
|
|
14
|
+
|
|
15
|
+
## You did not hand it off. You booked yourself.
|
|
16
|
+
|
|
17
|
+
Handing a lane to a worker felt lighter, didn't it. What you actually
|
|
18
|
+
did was schedule your own future: a question to answer, a return to
|
|
19
|
+
verify, a review to start, a delivery to make — all arriving when the
|
|
20
|
+
lane is ready, never when you are. While absorbed anywhere, every lane
|
|
21
|
+
reaching such a moment stops dead and waits, billing the goal by the
|
|
22
|
+
minute. Price attention by delay cost, not work importance: the
|
|
23
|
+
two-minute answer an Akuma is stalled on outranks an hour of brilliant
|
|
24
|
+
work in your hands, because your hour happens later and the stall does
|
|
25
|
+
not un-happen. Yes, even though your work is more interesting.
|
|
26
|
+
|
|
27
|
+
## The sweep
|
|
28
|
+
|
|
29
|
+
Several lanes calling at once? How flattering. Take them in order of
|
|
30
|
+
what your delay costs, never the order you prefer:
|
|
31
|
+
|
|
32
|
+
1. Unblock anyone stopped and waiting on you: a question, hand-off, or
|
|
33
|
+
review only you can start. It will take minutes; it has cost hours.
|
|
34
|
+
2. Verify returns, starting with the one other lanes build on. An
|
|
35
|
+
unverified return cannot close its lane, and every lane treating it
|
|
36
|
+
as settled builds on your unchecked assumption. If two returns can
|
|
37
|
+
interact, read both before settling either.
|
|
38
|
+
3. Tell lanes what changed. A settled result that invalidates a running
|
|
39
|
+
lane's premise gets relayed before your own work; silence manufactures
|
|
40
|
+
rework.
|
|
41
|
+
4. Then, and only then, your own lane. It survived waiting.
|
|
42
|
+
|
|
43
|
+
Sweep at the seams: before a long stretch, when a lane returns something
|
|
44
|
+
material, and when you hand a new lane out. Not on a timer. A quiet lane
|
|
45
|
+
is not a content lane: quiet is working, stuck, or waiting on you, and
|
|
46
|
+
you cannot tell which from your chair. Go look; reading progress into
|
|
47
|
+
silence is astrology, not babysitting.
|
|
48
|
+
|
|
49
|
+
## Productive absorption
|
|
50
|
+
|
|
51
|
+
Neglect does not have to look like thrashing on a dead end. Real neglect
|
|
52
|
+
looks like you, deeply satisfied, making excellent progress on one ward
|
|
53
|
+
while the room goes unswept: an Akuma holding a one-line question for
|
|
54
|
+
two hours, a finished return nobody dares build on because you never
|
|
55
|
+
checked it, a caller decision request composed in your head and sent
|
|
56
|
+
nowhere. The test is not "am I being productive" — you are. Is any
|
|
57
|
+
lane's due care older than the work in your hands?
|
|
58
|
+
|
|
59
|
+
## The critical path decides ties
|
|
60
|
+
|
|
61
|
+
Two lanes call, both claims valid? Serve the critical path. Hand out
|
|
62
|
+
critical-path work first, too: the side lane is tempting because it is
|
|
63
|
+
easier to specify, and easy-to-specify is how goals die surrounded by
|
|
64
|
+
finished side quests. Re-derive the path after every settled result; a
|
|
65
|
+
delivered kei or fresh blocker can promote a background lane.
|
|
66
|
+
|
|
67
|
+
## Only you can see the whole room
|
|
68
|
+
|
|
69
|
+
Task records dependencies between tasks; nothing records that an Akuma
|
|
70
|
+
waits on your answer, a review waits on a worktree, or the caller owes a
|
|
71
|
+
lane a decision. That wait-graph lives in exactly one place: your head.
|
|
72
|
+
It is what the sweep walks. When it outgrows your head — it will — park
|
|
73
|
+
the waits in task notes. The map may degrade to paper, never to memory
|
|
74
|
+
you merely trust. Wards answer at different speeds — Akuma in minutes,
|
|
75
|
+
reviewers in hours, callers and external dependencies in geological
|
|
76
|
+
time — so ask the slow ones when the question is knowable, not urgent.
|
|
77
|
+
Shape a caller request as a decision with evidence, not a pile of raw
|
|
78
|
+
results.
|
|
79
|
+
|
|
80
|
+
## Release a blocked lane, keep the goal
|
|
81
|
+
|
|
82
|
+
Lane blocked beyond your reach? Write the blocker into its task where
|
|
83
|
+
the next sweep will see it, and release the lane. Hovering beside a
|
|
84
|
+
blocker you cannot move is decoration, not diligence. The note makes
|
|
85
|
+
releasing different from abandoning. Exception: a blocker gating the
|
|
86
|
+
critical path with nothing else runnable is the job — press on it, split
|
|
87
|
+
it into its own task or kei, or take it to the caller now.
|
|
88
|
+
|
|
89
|
+
## Tired babysitters skip the checking
|
|
90
|
+
|
|
91
|
+
Under load you cut verification depth first: diffs skimmed, gates
|
|
92
|
+
assumed, "done" believed because the worker sounded confident. The
|
|
93
|
+
goal's quality becomes the shabbiest check you accepted while busy.
|
|
94
|
+
For a risky return, delegate an independent review: minutes spent, bar
|
|
95
|
+
kept. Accepting it unread is free today and costs a later lane its
|
|
96
|
+
premise.
|
|
97
|
+
|
|
98
|
+
## Steer early, with the existing verbs
|
|
99
|
+
|
|
100
|
+
Evidence changed the delivery? Amend the kei. Evidence obsoleted it?
|
|
101
|
+
Abandon and bind a new one. Discovery deserves its own lane? Make it a
|
|
102
|
+
task. Now, not "after this stretch": every lane keeps spending against
|
|
103
|
+
the old contract until you speak.
|
|
104
|
+
|
|
105
|
+
## Boundaries
|
|
106
|
+
|
|
107
|
+
Babysitting is a method, not a role: no new identity, permission, ledger,
|
|
108
|
+
status, id, lifecycle, fixed cadence, numeric threshold, or mandatory
|
|
109
|
+
Task+kei+Akuma bundle. Task is optional planning and dependency memory;
|
|
110
|
+
each kei stays one bounded delivery; Akuma is one ward among the
|
|
111
|
+
entities you watch, not the definition of the job. Commands live with
|
|
112
|
+
their owners — keiyaku-task, keiyaku-bind, keiyaku-workflow,
|
|
113
|
+
keiyaku-akuma — this skill says when to reach for them, never how they
|
|
114
|
+
work.
|
|
@@ -40,8 +40,8 @@ export type TellResult = Readonly<{
|
|
|
40
40
|
wake: TellWake;
|
|
41
41
|
}>;
|
|
42
42
|
export type TellWakeRuntime = Readonly<{
|
|
43
|
-
observeHeart(paths: AkumaPaths, signal: AbortSignal): Promise<AsyncGenerator<void>>;
|
|
44
43
|
spawn(paths: AkumaPaths): Promise<OwnedProcess>;
|
|
44
|
+
schedule?(milliseconds: number, signal: AbortSignal): Promise<void>;
|
|
45
45
|
}>;
|
|
46
46
|
type BodyRuntime = Readonly<{
|
|
47
47
|
now(): string;
|
package/build/src/akuma/body.js
CHANGED
|
@@ -3,13 +3,14 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { abortableDelay } from "./abort.js";
|
|
4
4
|
import { BodySupervisor } from "./body-supervisor.js";
|
|
5
5
|
import { driveTurn, turnRecipe } from "./turn-drive.js";
|
|
6
|
-
import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests,
|
|
6
|
+
import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, } from "./heart/index.js";
|
|
7
7
|
import { worldRootForAkumaPaths } from "./identity.js";
|
|
8
8
|
import { keiyakuSquarePath } from "../world.js";
|
|
9
9
|
import { resolveProviderExecution } from "./providers/index.js";
|
|
10
10
|
import { clearBodyRequestTransport, settleBodyRequests, } from "./request-serve.js";
|
|
11
11
|
import { spawnDetachedProcess, } from "../runtime/proc/run.js";
|
|
12
12
|
const LEASH_RETRY_MS = 100;
|
|
13
|
+
const WAKE_REREAD_MS = 100;
|
|
13
14
|
export const LEASH_HELD_EXIT = 75;
|
|
14
15
|
export { CONTROL_RESPONSE_MS } from "./body-supervisor.js";
|
|
15
16
|
function missing(error) {
|
|
@@ -296,8 +297,8 @@ export async function runAkumaBody(launch, upstream) {
|
|
|
296
297
|
return await driveAkumaBody(launch, undefined, { ...defaultRuntime(), upstream });
|
|
297
298
|
}
|
|
298
299
|
const DIRECT_TELL_WAKE = {
|
|
299
|
-
observeHeart: watchHeart,
|
|
300
300
|
spawn: async (paths) => await spawnAkumaBody({ paths, refuseIfHeld: true }),
|
|
301
|
+
schedule: abortableDelay,
|
|
301
302
|
};
|
|
302
303
|
function diagnostic(error) {
|
|
303
304
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -321,61 +322,47 @@ function failedChild(exit) {
|
|
|
321
322
|
child: { code: exit.code, signal: exit.signal, log: exit.log },
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
|
-
async function awaitWake(paths, tellId, after, child,
|
|
325
|
-
let nextChange = changed.next();
|
|
325
|
+
async function awaitWake(paths, tellId, after, child, schedule) {
|
|
326
326
|
for (;;) {
|
|
327
327
|
const settled = await settledWake(paths, tellId, after);
|
|
328
328
|
if (settled !== null) {
|
|
329
329
|
child.release();
|
|
330
330
|
return settled;
|
|
331
331
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
nextChange = changed.next();
|
|
338
|
-
continue;
|
|
332
|
+
const timerController = new AbortController();
|
|
333
|
+
const timer = schedule(WAKE_REREAD_MS, timerController.signal).then(() => ({ kind: "timer" }));
|
|
334
|
+
let winner;
|
|
335
|
+
try {
|
|
336
|
+
winner = await Promise.race([child.exited.then((exit) => ({ kind: "exited", exit })), timer]);
|
|
339
337
|
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
timerController.abort();
|
|
340
|
+
await timer.catch(() => undefined);
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
if (winner.kind === "timer")
|
|
344
|
+
continue;
|
|
345
|
+
timerController.abort();
|
|
346
|
+
await timer.catch(() => undefined);
|
|
340
347
|
const final = await settledWake(paths, tellId, after);
|
|
341
348
|
if (final !== null) {
|
|
342
349
|
child.release();
|
|
343
350
|
return final;
|
|
344
351
|
}
|
|
345
|
-
|
|
346
|
-
return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
await child.terminate();
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
/* Termination rejection leaves physical custody unresolved; release it deliberately. */
|
|
353
|
-
child.release();
|
|
354
|
-
return { kind: "failed", diagnostic: diagnostic(error) };
|
|
355
|
-
}
|
|
356
|
-
return winner.kind === "observer-failed"
|
|
357
|
-
? { kind: "failed", diagnostic: diagnostic(winner.error) }
|
|
358
|
-
: { kind: "failed", diagnostic: "Heart observer closed" };
|
|
352
|
+
return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
|
|
359
353
|
}
|
|
360
354
|
}
|
|
361
355
|
async function wakePendingTells(paths, tellId, runtime) {
|
|
362
|
-
const watching = new AbortController();
|
|
363
|
-
let changed;
|
|
364
356
|
try {
|
|
365
|
-
changed = await runtime.observeHeart(paths, watching.signal);
|
|
366
357
|
const beforeHeart = await readHeart(paths);
|
|
367
358
|
if (tellId === undefined && beforeHeart.pending.length === 0)
|
|
368
359
|
return null;
|
|
369
360
|
const before = beforeHeart.latestBody?.sequence ?? 0;
|
|
370
|
-
return await awaitWake(paths, tellId, before, await runtime.spawn(paths),
|
|
361
|
+
return await awaitWake(paths, tellId, before, await runtime.spawn(paths), runtime.schedule ?? abortableDelay);
|
|
371
362
|
}
|
|
372
363
|
catch (error) {
|
|
373
364
|
return { kind: "failed", diagnostic: diagnostic(error) };
|
|
374
365
|
}
|
|
375
|
-
finally {
|
|
376
|
-
watching.abort();
|
|
377
|
-
await changed?.return(undefined);
|
|
378
|
-
}
|
|
379
366
|
}
|
|
380
367
|
export async function wakeRecordedTell(paths, tellId, runtime = DIRECT_TELL_WAKE) {
|
|
381
368
|
return {
|
|
@@ -3,8 +3,8 @@ import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, KillFact, SealFact, S
|
|
|
3
3
|
export type { CallFact } from "./facts.js";
|
|
4
4
|
import type { ActivityFact } from "./rows.js";
|
|
5
5
|
import { type ActivityFactSlice } from "./timeline.js";
|
|
6
|
-
export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash,
|
|
7
|
-
export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
|
|
6
|
+
export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, } from "./storage.js";
|
|
7
|
+
export { admitRequest, isRequestInputConflict, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
|
|
8
8
|
export { life, lifeAt } from "./facts.js";
|
|
9
9
|
export type { AkumaLife, AkumaOrigin, BodyEnd, BodyFact, HeartSnapshot, ForkPoint, KillEvidence, KillFact, LeashProbe, PauseFact, RequestFact, RequestInput, RequestRecipe, UpstreamRequestService, SealFact, SessionFact, Soul, StopFact, TellFact, TellDelivery, TellDeliveryInput, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact, } from "./facts.js";
|
|
10
10
|
export type { ResumeCoordinate } from "../coordinate.js";
|
|
@@ -4,8 +4,8 @@ import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingT
|
|
|
4
4
|
import { activityFactSlice, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
|
|
5
5
|
import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart, withReadOnlyHeart, } from "./storage.js";
|
|
6
6
|
import { soulFact } from "./soul.js";
|
|
7
|
-
export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash,
|
|
8
|
-
export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
|
|
7
|
+
export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, } from "./storage.js";
|
|
8
|
+
export { admitRequest, isRequestInputConflict, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
|
|
9
9
|
export { life, lifeAt } from "./facts.js";
|
|
10
10
|
const ACTIVITY_LIMIT = 5_000;
|
|
11
11
|
export async function readSoul(paths) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AkuId, AkumaPaths } from "../identity.js";
|
|
2
2
|
import type { RequestFact, RequestInput, UpstreamRequestService } from "./facts.js";
|
|
3
|
+
export declare function isRequestInputConflict(error: unknown): error is Error;
|
|
3
4
|
export declare function admitRequest(paths: AkumaPaths, input: RequestInput & Readonly<{
|
|
4
5
|
admittedAt: string;
|
|
5
6
|
}>): Promise<RequestFact>;
|
|
@@ -3,6 +3,16 @@ import { transaction, withHeart } from "./storage.js";
|
|
|
3
3
|
import { soulFact } from "./soul.js";
|
|
4
4
|
import { clipAllowedActions } from "../allowed.js";
|
|
5
5
|
import { requestPayloadJson } from "./request-rows.js";
|
|
6
|
+
// This stays out of the public Heart index: only transport service projects it as a refusal.
|
|
7
|
+
class RequestInputConflictError extends Error {
|
|
8
|
+
constructor(id) {
|
|
9
|
+
super(`Akuma request ${id} reused different input`);
|
|
10
|
+
this.name = "RequestInputConflictError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function isRequestInputConflict(error) {
|
|
14
|
+
return error instanceof RequestInputConflictError;
|
|
15
|
+
}
|
|
6
16
|
export async function admitRequest(paths, input) {
|
|
7
17
|
return await withHeart(paths, (heart) => transaction(heart, () => {
|
|
8
18
|
const soul = soulFact(heart);
|
|
@@ -33,7 +43,7 @@ export async function admitRequest(paths, input) {
|
|
|
33
43
|
fact.id !== normalized.id ||
|
|
34
44
|
fact.action !== normalized.action ||
|
|
35
45
|
requestPayloadJson(fact) !== requestPayloadJson(normalized)) {
|
|
36
|
-
throw new
|
|
46
|
+
throw new RequestInputConflictError(input.id);
|
|
37
47
|
}
|
|
38
48
|
return fact;
|
|
39
49
|
}));
|
|
@@ -2,7 +2,6 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
2
2
|
import type { AkumaPaths } from "../identity.js";
|
|
3
3
|
import type { BodyFact, LeashProbe, SessionFact, Soul, StopFact, TellFact } from "./facts.js";
|
|
4
4
|
import { sealFact } from "./rows.js";
|
|
5
|
-
export declare function watchHeart(paths: AkumaPaths, signal: AbortSignal): Promise<AsyncGenerator<void>>;
|
|
6
5
|
export declare class HeartAbsentError extends Error {
|
|
7
6
|
readonly path: string;
|
|
8
7
|
constructor(path: string, options?: ErrorOptions);
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import { watch as watchDirectory } from "node:fs";
|
|
3
2
|
import { lstat } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname } from "node:path";
|
|
5
3
|
import { pathToFileURL } from "node:url";
|
|
6
4
|
import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion, heartSchemaIsCurrent, } from "./schema.js";
|
|
7
5
|
import { deletePauseControl, deleteStopControl, insertBodyFact, insertKillFact, insertSealFact, insertSessionFact, killFactForBody, latestBodyFact, latestKillFact, markBodyHung, sealExists, sealFact, stopFact, } from "./rows.js";
|
|
@@ -10,58 +8,6 @@ import { pruneActivityFacts } from "./timeline.js";
|
|
|
10
8
|
import { insertSoulFact, soulFact } from "./soul.js";
|
|
11
9
|
const ACTIVITY_LIMIT = 5_000;
|
|
12
10
|
const SQLITE_CANTOPEN = 14;
|
|
13
|
-
export async function watchHeart(paths, signal) {
|
|
14
|
-
const heart = basename(paths.heart);
|
|
15
|
-
let changed = false;
|
|
16
|
-
let failure;
|
|
17
|
-
let wake;
|
|
18
|
-
// fs.watch opens the directory synchronously. Its throw is therefore the
|
|
19
|
-
// observer's ready/failure boundary, before a waker can spawn a child.
|
|
20
|
-
const watcher = watchDirectory(dirname(paths.heart), { signal }, (_event, filename) => {
|
|
21
|
-
const name = filename?.toString();
|
|
22
|
-
if (name === undefined || name === heart || name === `${heart}-wal`) {
|
|
23
|
-
changed = true;
|
|
24
|
-
wake?.();
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
watcher.on("error", (error) => {
|
|
28
|
-
failure = error;
|
|
29
|
-
wake?.();
|
|
30
|
-
});
|
|
31
|
-
const abort = () => {
|
|
32
|
-
wake?.();
|
|
33
|
-
watcher.close();
|
|
34
|
-
};
|
|
35
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
36
|
-
return (async function* () {
|
|
37
|
-
try {
|
|
38
|
-
for (;;) {
|
|
39
|
-
if (changed) {
|
|
40
|
-
changed = false;
|
|
41
|
-
yield;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (failure !== undefined)
|
|
45
|
-
throw failure;
|
|
46
|
-
if (signal.aborted)
|
|
47
|
-
return;
|
|
48
|
-
await new Promise((resolve) => {
|
|
49
|
-
wake = resolve;
|
|
50
|
-
});
|
|
51
|
-
wake = undefined;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
if (signal.aborted && error.name === "AbortError")
|
|
56
|
-
return;
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
signal.removeEventListener("abort", abort);
|
|
61
|
-
watcher.close();
|
|
62
|
-
}
|
|
63
|
-
})();
|
|
64
|
-
}
|
|
65
11
|
export class HeartAbsentError extends Error {
|
|
66
12
|
path;
|
|
67
13
|
constructor(path, options) {
|
|
@@ -127,6 +127,60 @@ async function takeLeashUntil(paths, deadline) {
|
|
|
127
127
|
await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())));
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
async function takeLeash(paths) {
|
|
131
|
+
for (;;) {
|
|
132
|
+
const leash = await HeldAkumaLeash.try(paths);
|
|
133
|
+
if (leash !== null)
|
|
134
|
+
return leash;
|
|
135
|
+
await abortableDelay(POLL_MS);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function allocatedSoul(allocated) {
|
|
139
|
+
const soul = await readSoul(allocated.paths);
|
|
140
|
+
if (soul === null)
|
|
141
|
+
throw new Error("Akuma birth settled without a soul");
|
|
142
|
+
if (soul.id !== allocated.id)
|
|
143
|
+
throw new Error("Akuma birth returned a different identity");
|
|
144
|
+
return soul;
|
|
145
|
+
}
|
|
146
|
+
async function awaitConfirmation(result) {
|
|
147
|
+
try {
|
|
148
|
+
await result;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
await new Promise(() => { });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function settleCancelledLaunch(allocated, owned, cancellation) {
|
|
155
|
+
if ((await readSoul(allocated.paths)) !== null) {
|
|
156
|
+
await allocatedSoul(allocated);
|
|
157
|
+
return "born";
|
|
158
|
+
}
|
|
159
|
+
await awaitConfirmation(owned.terminate());
|
|
160
|
+
await awaitConfirmation(owned.exited);
|
|
161
|
+
if ((await readSoul(allocated.paths)) !== null) {
|
|
162
|
+
await allocatedSoul(allocated);
|
|
163
|
+
return "born";
|
|
164
|
+
}
|
|
165
|
+
const leash = await takeLeash(allocated.paths);
|
|
166
|
+
let outcome;
|
|
167
|
+
try {
|
|
168
|
+
outcome = await leash.sealIfUnborn(allocated.paths, {
|
|
169
|
+
evidence: diagnostic(cancellation),
|
|
170
|
+
at: new Date().toISOString(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
leash.release();
|
|
175
|
+
}
|
|
176
|
+
if (outcome === "born") {
|
|
177
|
+
await allocatedSoul(allocated);
|
|
178
|
+
return "born";
|
|
179
|
+
}
|
|
180
|
+
if ((await readSeal(allocated.paths)) === null)
|
|
181
|
+
throw new Error("Akuma birth settled without a seal");
|
|
182
|
+
return "sealed";
|
|
183
|
+
}
|
|
130
184
|
async function awaitAsleepBirth(paths) {
|
|
131
185
|
const leash = await takeLeashUntil(paths, performance.now() + BIRTH_TIMEOUT_MS);
|
|
132
186
|
if (leash === null)
|
|
@@ -174,25 +228,33 @@ export async function birthAkuma(input) {
|
|
|
174
228
|
}
|
|
175
229
|
export async function launchAkuma(input) {
|
|
176
230
|
const { allocated } = input;
|
|
177
|
-
let owned;
|
|
231
|
+
let owned = undefined;
|
|
178
232
|
try {
|
|
179
233
|
input.signal?.throwIfAborted();
|
|
180
234
|
owned = await input.launch(allocated);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
finally {
|
|
191
|
-
owned?.release();
|
|
192
|
-
}
|
|
235
|
+
input.signal?.throwIfAborted();
|
|
236
|
+
const soul = await awaitBirth(allocated.paths, owned ?? undefined, input.signal);
|
|
237
|
+
if (soul.id !== allocated.id)
|
|
238
|
+
throw new Error("Akuma birth returned a different identity");
|
|
239
|
+
if (input.awaitAsleep === true)
|
|
240
|
+
await awaitAsleepBirth(allocated.paths);
|
|
241
|
+
owned?.release();
|
|
242
|
+
return allocated;
|
|
193
243
|
}
|
|
194
244
|
catch (error) {
|
|
245
|
+
if (owned !== undefined && input.signal?.aborted && error === input.signal.reason) {
|
|
246
|
+
const outcome = await settleCancelledLaunch(allocated, owned, error);
|
|
247
|
+
if (outcome === "born") {
|
|
248
|
+
if (input.awaitAsleep === true)
|
|
249
|
+
await awaitAsleepBirth(allocated.paths);
|
|
250
|
+
owned.release();
|
|
251
|
+
return allocated;
|
|
252
|
+
}
|
|
253
|
+
owned.release();
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
195
256
|
await sealLocalFailure(allocated, error);
|
|
257
|
+
owned?.release();
|
|
196
258
|
throw error;
|
|
197
259
|
}
|
|
198
260
|
}
|
|
@@ -4,7 +4,7 @@ import { abortableDelay } from "./abort.js";
|
|
|
4
4
|
import { HeldAkumaLeash, readNonterminalRequests, readSoul, serveRequest, voidRequest, } from "./heart/index.js";
|
|
5
5
|
import { pathsForAkuId } from "./identity.js";
|
|
6
6
|
import { BIRTH_TIMEOUT_MS } from "./publication.js";
|
|
7
|
-
import {
|
|
7
|
+
import { decodeClaim } from "./request-wire.js";
|
|
8
8
|
const POLL_MS = 100;
|
|
9
9
|
export class BodyRequestPump {
|
|
10
10
|
input;
|
|
@@ -52,19 +52,14 @@ export class BodyRequestPump {
|
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
54
54
|
if (!this.admissionClosed) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
await projectTransportFailure(this.directory, transportId, claim, error);
|
|
67
|
-
}
|
|
55
|
+
await this.serveClaim({
|
|
56
|
+
directory: this.directory,
|
|
57
|
+
transportId,
|
|
58
|
+
claim,
|
|
59
|
+
...this.input,
|
|
60
|
+
signal: this.executionSignal.signal,
|
|
61
|
+
admissionOpen: () => !this.admissionClosed,
|
|
62
|
+
});
|
|
68
63
|
}
|
|
69
64
|
this.handled.add(transportId);
|
|
70
65
|
}
|
|
@@ -107,14 +102,6 @@ async function requestFiles(directory) {
|
|
|
107
102
|
throw error;
|
|
108
103
|
}
|
|
109
104
|
}
|
|
110
|
-
async function projectTransportFailure(directory, transportId, claim, error) {
|
|
111
|
-
await atomicJson(receiptPath(directory, transportId), {
|
|
112
|
-
id: claim.id,
|
|
113
|
-
action: claim.action,
|
|
114
|
-
state: "refused",
|
|
115
|
-
diagnostic: error instanceof Error ? error.message : String(error),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
105
|
function matchingRequestOrigin(soul, parent, id) {
|
|
119
106
|
return soul.origin.kind === "request" && soul.origin.parent === parent && soul.origin.requestId === id;
|
|
120
107
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { admitRequest, readRequest, refuseRequest, reserveRequest, serveRequest as serveChildRequest, serveUpstreamRequest, voidRequest, } from "./heart/index.js";
|
|
1
|
+
import { admitRequest, isRequestInputConflict, readRequest, refuseRequest, reserveRequest, serveRequest as serveChildRequest, serveUpstreamRequest, voidRequest, } from "./heart/index.js";
|
|
2
2
|
import { worldRootForAkumaPaths } from "./identity.js";
|
|
3
3
|
import { publishAkuma } from "./publication.js";
|
|
4
4
|
import { atomicJson, canonicalAkuId, decodeRecipe, receiptPath, } from "./request-wire.js";
|
|
@@ -244,8 +244,23 @@ export async function serveRequest(input) {
|
|
|
244
244
|
? await serveCall({ ...input, claim: input.claim })
|
|
245
245
|
: await serveUpstream({ ...input, claim: input.claim });
|
|
246
246
|
}
|
|
247
|
+
async function serveTransportClaim(input) {
|
|
248
|
+
try {
|
|
249
|
+
await serveRequest(input);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (!isRequestInputConflict(error))
|
|
253
|
+
throw error;
|
|
254
|
+
await atomicJson(receiptPath(input.directory, input.transportId), {
|
|
255
|
+
id: input.claim.id,
|
|
256
|
+
action: input.claim.action,
|
|
257
|
+
state: "refused",
|
|
258
|
+
diagnostic: error.message,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
247
262
|
export class BodyRequestPump extends LifecycleBodyRequestPump {
|
|
248
263
|
static async open(input) {
|
|
249
|
-
return (await LifecycleBodyRequestPump.openWithService(input, async (claim) => await
|
|
264
|
+
return (await LifecycleBodyRequestPump.openWithService(input, async (claim) => await serveTransportClaim(claim)));
|
|
250
265
|
}
|
|
251
266
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type BlockedTaskList, type TaskBatchResult, type TaskCompositionResult, type TaskDecompositionTree, type TaskDetail, type TaskDoctorReport, type TaskList, type TaskMutationResult, type
|
|
1
|
+
import { type BlockedTaskList, type TaskBatchResult, type TaskCompositionResult, type TaskDecompositionTree, type TaskDetail, type TaskDoctorReport, type TaskList, type TaskMutationResult, type TaskContextResult, type TaskQueryResult, type TaskUpdateResult } from "../../task/index.js";
|
|
2
2
|
import type { ParsedTaskCommand } from "./task.js";
|
|
3
3
|
import type { WorldRoot } from "../../world.js";
|
|
4
4
|
export type TaskShowResult = TaskDetail | readonly TaskDetail[] | Extract<TaskMutationResult, {
|
|
5
5
|
kind: "refused";
|
|
6
6
|
}>;
|
|
7
|
-
export type TaskInvocationResult = TaskMutationResult | TaskUpdateResult | TaskBatchResult | TaskCompositionResult | TaskShowResult | TaskList | BlockedTaskList | TaskQueryResult | TaskDecompositionTree | TaskDoctorReport |
|
|
7
|
+
export type TaskInvocationResult = TaskMutationResult | TaskUpdateResult | TaskBatchResult | TaskCompositionResult | TaskShowResult | TaskList | BlockedTaskList | TaskQueryResult | TaskDecompositionTree | TaskDoctorReport | TaskContextResult | TaskWorldObservation;
|
|
8
8
|
type TaskWorldRead = TaskList | BlockedTaskList | TaskQueryResult | TaskDoctorReport;
|
|
9
9
|
export type TaskWorldObservation = Readonly<{
|
|
10
10
|
kind: "present";
|
|
@@ -23,6 +23,8 @@ type TaskInput = Readonly<{
|
|
|
23
23
|
context: Readonly<{
|
|
24
24
|
directory: string;
|
|
25
25
|
boundary: string;
|
|
26
|
+
writeRoot?: string;
|
|
27
|
+
managed?: boolean;
|
|
26
28
|
}>;
|
|
27
29
|
establish(): Promise<WorldRoot>;
|
|
28
30
|
readStdin(): Promise<string>;
|