@astrosheep/keiyaku 4.5.0 → 4.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +4 -2
- package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/package.json +9 -3
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +16 -5
- package/build/integrations/pi/keiyaku.ts +159 -0
- package/build/src/akuma/abort.js +9 -9
- package/build/src/akuma/akuma-handle.d.ts +4 -2
- package/build/src/akuma/akuma-handle.js +14 -2
- package/build/src/akuma/akuma-product.d.ts +8 -2
- package/build/src/akuma/akuma-product.js +9 -4
- package/build/src/akuma/body.d.ts +4 -0
- package/build/src/akuma/body.js +21 -14
- package/build/src/akuma/heart/index.js +7 -3
- package/build/src/akuma/heart/rows.d.ts +1 -1
- package/build/src/akuma/heart/rows.js +3 -3
- package/build/src/akuma/heart/storage.d.ts +1 -0
- package/build/src/akuma/heart/storage.js +28 -6
- package/build/src/akuma/identity.d.ts +1 -0
- package/build/src/akuma/identity.js +7 -0
- package/build/src/akuma/index.d.ts +1 -1
- package/build/src/akuma/projection.d.ts +18 -2
- package/build/src/akuma/projection.js +11 -1
- package/build/src/akuma/providers/claude/index.js +68 -37
- package/build/src/akuma/publication.js +3 -0
- package/build/src/akuma/request-lifecycle.d.ts +45 -0
- package/build/src/akuma/request-lifecycle.js +171 -0
- package/build/src/akuma/request-serve.d.ts +9 -32
- package/build/src/akuma/request-serve.js +47 -173
- package/build/src/akuma/request-wire.js +1 -1
- package/build/src/akuma/requests.js +3 -2
- package/build/src/cli/commands/akuma-invoke.d.ts +9 -0
- package/build/src/cli/commands/akuma-invoke.js +9 -6
- package/build/src/cli/commands/akuma.d.ts +1 -0
- package/build/src/cli/commands/akuma.js +16 -6
- package/build/src/cli/commands/contract.d.ts +3 -2
- package/build/src/cli/commands/contract.js +5 -2
- package/build/src/cli/commands/status-set.d.ts +4 -0
- package/build/src/cli/commands/status-set.js +61 -0
- package/build/src/cli/invoke.js +5 -0
- package/build/src/cli/parse.js +1 -1
- package/build/src/cli/render/akuma-activity.js +11 -0
- package/build/src/cli/render/akuma.js +7 -0
- package/build/src/cli/render/status-set.d.ts +5 -0
- package/build/src/cli/render/status-set.js +14 -0
- package/build/src/cli/render/text.js +3 -0
- package/build/src/cli/result.d.ts +15 -1
- package/build/src/cli/runtime.js +2 -0
- package/build/src/cli/square-edge.d.ts +2 -1
- package/build/src/cli/square-edge.js +60 -33
- package/build/src/contract-worktree.js +18 -5
- package/build/src/library/akuma-creation.d.ts +1 -1
- package/build/src/library/akuma-creation.js +6 -2
- package/build/src/library/fleet.d.ts +12 -1
- package/build/src/library/fleet.js +21 -5
- package/build/src/library/repo.js +3 -1
- package/build/src/runtime/proc/windows-launch.exe +0 -0
- package/build/src/world.d.ts +2 -0
- package/build/src/world.js +4 -0
- package/package.json +8 -3
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keiyaku-harness",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./opencode.js",
|
|
6
|
-
"keywords": [
|
|
7
|
-
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"pi": {
|
|
10
|
+
"skills": [
|
|
11
|
+
"./skills"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
8
14
|
}
|
|
@@ -84,6 +84,7 @@ choose `--interrupt` when the current attempt itself is the problem.
|
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
keiyaku history <aku/...|@alias> --last
|
|
87
|
+
keiyaku history <aku/...|@alias> --id <historyId>
|
|
87
88
|
keiyaku history <aku/...|@alias> [--before <N> | --since <N>] [--limit <N>]
|
|
88
89
|
```
|
|
89
90
|
|
|
@@ -91,9 +92,17 @@ keiyaku history <aku/...|@alias> [--before <N> | --since <N>] [--limit <N>]
|
|
|
91
92
|
and says so plainly when no answer exists yet. Snapshot rows elsewhere may
|
|
92
93
|
clip long text; the terminal answer from `call`/`wait` and the bytes from
|
|
93
94
|
`--last` are never clipped — when you need the full result, take it from one
|
|
94
|
-
of those.
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
of those. Every completed answered or failed outcome carries one Heart-owned
|
|
96
|
+
public `historyId` shaped as `turn/<positive-safe-integer>` in `status`, `wait`,
|
|
97
|
+
and history output. Use `--id` with that same value to read exactly one retained
|
|
98
|
+
outcome: text writes the complete answer or diagnostic bytes without clipping
|
|
99
|
+
or framing. Provider-native history coordinates remain private. A malformed or
|
|
100
|
+
unknown ID is a typed nonzero refusal. `--id` is mutually exclusive with
|
|
101
|
+
`--last`, `--before`, `--since`, and `--limit`.
|
|
102
|
+
|
|
103
|
+
Cursor reads page the activity timeline; `--before` and `--since` are exclusive
|
|
104
|
+
sequence cursors. `--limit` defaults to 50 and accepts at most 5000 semantic
|
|
105
|
+
rows.
|
|
97
106
|
|
|
98
107
|
## Stop
|
|
99
108
|
|
|
@@ -116,5 +125,7 @@ keiyaku fork <aku/...|@alias> --at <historyId> [--alias @name]
|
|
|
116
125
|
`fork` starts a child from one exact retained answered-turn coordinate and
|
|
117
126
|
leaves the source untouched. It is a provider capability, not a guarantee:
|
|
118
127
|
when the provider cannot fork from that turn, the command refuses rather than
|
|
119
|
-
fabricating a fresh start.
|
|
120
|
-
|
|
128
|
+
fabricating a fresh start. Pass the same public `historyId` exposed by status,
|
|
129
|
+
wait, or history; Heart privately resolves the provider coordinate. The ID must
|
|
130
|
+
name an answered outcome with a provider fork point. Failed outcomes are not
|
|
131
|
+
forkable.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
type Section = Readonly<{ kind?: string; value?: Readonly<{ rows?: readonly Record<string, unknown>[] }> }>;
|
|
7
|
+
type Report = Readonly<{
|
|
8
|
+
contracts?: Section;
|
|
9
|
+
tasks?: Section;
|
|
10
|
+
akuma?: Section;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
type ExecResult = Readonly<{ stdout: string; stderr: string; code: number }>;
|
|
14
|
+
|
|
15
|
+
const REFRESH_TIMEOUT_MS = 10_000;
|
|
16
|
+
|
|
17
|
+
function rows(section: Section | undefined): readonly Record<string, unknown>[] {
|
|
18
|
+
return section?.kind === "present" && Array.isArray(section.value?.rows) ? section.value.rows : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function failed(section: Section | undefined): boolean {
|
|
22
|
+
return section?.kind === "failed";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function contractNeedsAttention(row: Record<string, unknown>): boolean {
|
|
26
|
+
const gates = row.gates;
|
|
27
|
+
if (typeof gates === "object" && gates !== null && "reports" in gates && Array.isArray(gates.reports)) {
|
|
28
|
+
for (const report of gates.reports) {
|
|
29
|
+
if (
|
|
30
|
+
typeof report === "object" &&
|
|
31
|
+
report !== null &&
|
|
32
|
+
"current" in report &&
|
|
33
|
+
typeof report.current === "object" &&
|
|
34
|
+
report.current !== null &&
|
|
35
|
+
"kind" in report.current &&
|
|
36
|
+
report.current.kind === "attested" &&
|
|
37
|
+
"verdict" in report.current &&
|
|
38
|
+
report.current.verdict === "unsatisfied"
|
|
39
|
+
)
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return row.targetLag !== undefined &&
|
|
44
|
+
typeof row.targetLag === "object" &&
|
|
45
|
+
row.targetLag !== null &&
|
|
46
|
+
"kind" in row.targetLag
|
|
47
|
+
? row.targetLag.kind === "unknown"
|
|
48
|
+
: false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function taskNeedsAttention(row: Record<string, unknown>): boolean {
|
|
52
|
+
return row.disposition === "blocked";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function akumaNeedsAttention(row: Record<string, unknown>): boolean {
|
|
56
|
+
return row.life === "stillborn" || row.life === "hung" || row.life === "untidy";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function summary(report: Report): string {
|
|
60
|
+
const contractRows = rows(report.contracts);
|
|
61
|
+
const taskRows = rows(report.tasks);
|
|
62
|
+
const akumaRows = rows(report.akuma);
|
|
63
|
+
const attention =
|
|
64
|
+
Number(failed(report.contracts)) +
|
|
65
|
+
Number(failed(report.tasks)) +
|
|
66
|
+
Number(failed(report.akuma)) +
|
|
67
|
+
contractRows.filter(contractNeedsAttention).length +
|
|
68
|
+
taskRows.filter(taskNeedsAttention).length +
|
|
69
|
+
akumaRows.filter(akumaNeedsAttention).length;
|
|
70
|
+
const parts = [`Keiyaku · ${contractRows.length} contracts`, `${akumaRows.length} fleet`, `${taskRows.length} tasks`];
|
|
71
|
+
if (attention > 0) parts.push(`! ${attention}`);
|
|
72
|
+
return parts.join(" · ");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseReport(result: ExecResult): Report | undefined {
|
|
76
|
+
if (result.code !== 0 || result.stdout.trim().length === 0) return undefined;
|
|
77
|
+
try {
|
|
78
|
+
const value: unknown = JSON.parse(result.stdout);
|
|
79
|
+
return typeof value === "object" && value !== null ? (value as Report) : undefined;
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function run(pi: ExtensionAPI, args: readonly string[], timeout = REFRESH_TIMEOUT_MS): Promise<ExecResult> {
|
|
86
|
+
const bundledCli = resolve(dirname(fileURLToPath(import.meta.url)), "../../src/cli/index.js");
|
|
87
|
+
if (existsSync(bundledCli)) return await pi.exec(process.execPath, [bundledCli, ...args], { timeout });
|
|
88
|
+
return await pi.exec("keiyaku", [...args], { timeout });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function lineForWidth(value: string, width: number): string {
|
|
92
|
+
const clean = value.replaceAll(/[\u0000-\u001f\u007f]/gu, " ");
|
|
93
|
+
return clean.length <= width ? clean : `${clean.slice(0, Math.max(0, width - 1))}…`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function overlay(lines: readonly string[], theme: Theme, done: () => void) {
|
|
97
|
+
return {
|
|
98
|
+
invalidate(): void {},
|
|
99
|
+
handleInput(data: string): void {
|
|
100
|
+
if (data === "\u001b" || data === "q" || data === "Q" || data === "\r") done();
|
|
101
|
+
},
|
|
102
|
+
render(width: number): string[] {
|
|
103
|
+
const inner = Math.max(20, Math.min(100, width - 4));
|
|
104
|
+
const border = theme.fg("border", "+");
|
|
105
|
+
const rule = theme.fg("border", "-".repeat(inner));
|
|
106
|
+
return [
|
|
107
|
+
`${border}${rule}${border}`,
|
|
108
|
+
`${theme.fg("border", "|")}${lineForWidth("Keiyaku", inner).padEnd(inner)}${theme.fg("border", "|")}`,
|
|
109
|
+
`${theme.fg("border", "|")}${" ".repeat(inner)}${theme.fg("border", "|")}`,
|
|
110
|
+
...lines.map(
|
|
111
|
+
(line) => `${theme.fg("border", "|")}${lineForWidth(line, inner).padEnd(inner)}${theme.fg("border", "|")}`,
|
|
112
|
+
),
|
|
113
|
+
`${theme.fg("border", "|")}${" ".repeat(inner)}${theme.fg("border", "|")}`,
|
|
114
|
+
`${theme.fg("border", "|")}${lineForWidth("esc/q/enter close", inner).padEnd(inner)}${theme.fg("border", "|")}`,
|
|
115
|
+
`${border}${rule}${border}`,
|
|
116
|
+
];
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default function keiyakuExtension(pi: ExtensionAPI): void {
|
|
122
|
+
let refreshInFlight: Promise<void> | undefined;
|
|
123
|
+
|
|
124
|
+
const refresh = async (ctx: Pick<ExtensionContext, "hasUI" | "ui">): Promise<void> => {
|
|
125
|
+
if (!ctx.hasUI) return;
|
|
126
|
+
refreshInFlight ??= (async () => {
|
|
127
|
+
const result = await run(pi, ["status", "--json"]);
|
|
128
|
+
const report = parseReport(result);
|
|
129
|
+
if (report === undefined) {
|
|
130
|
+
const diagnostic = result.stderr.trim() || "status unavailable";
|
|
131
|
+
ctx.ui.setWidget("keiyaku", [`Keiyaku · ${lineForWidth(diagnostic, 100)}`]);
|
|
132
|
+
} else ctx.ui.setWidget("keiyaku", [summary(report)]);
|
|
133
|
+
})().finally(() => {
|
|
134
|
+
refreshInFlight = undefined;
|
|
135
|
+
});
|
|
136
|
+
await refreshInFlight;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
140
|
+
await refresh(ctx);
|
|
141
|
+
});
|
|
142
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
143
|
+
await refresh(ctx);
|
|
144
|
+
});
|
|
145
|
+
pi.registerCommand("keiyaku", {
|
|
146
|
+
description: "Open the Kanshi world status",
|
|
147
|
+
handler: async (_args, ctx) => {
|
|
148
|
+
if (!ctx.hasUI) return;
|
|
149
|
+
const result = await run(pi, ["status"]);
|
|
150
|
+
const body =
|
|
151
|
+
result.code === 0 ? result.stdout.trimEnd().split("\n") : [result.stderr.trim() || "status unavailable"];
|
|
152
|
+
await ctx.ui.custom<void>((_tui, theme, _keybindings, done) => overlay(body, theme, done), {
|
|
153
|
+
overlay: true,
|
|
154
|
+
overlayOptions: { anchor: "center", width: 100, maxHeight: 40 },
|
|
155
|
+
});
|
|
156
|
+
await refresh(ctx);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
package/build/src/akuma/abort.js
CHANGED
|
@@ -2,10 +2,13 @@ export function abortable(operation, signal, disposeLate) {
|
|
|
2
2
|
signal.throwIfAborted();
|
|
3
3
|
return new Promise((resolve, reject) => {
|
|
4
4
|
let aborted = false;
|
|
5
|
+
let settled = false;
|
|
5
6
|
const abort = () => {
|
|
6
7
|
aborted = true;
|
|
7
|
-
if (
|
|
8
|
+
if (!settled) {
|
|
9
|
+
settled = true;
|
|
8
10
|
reject(signal.reason);
|
|
11
|
+
}
|
|
9
12
|
};
|
|
10
13
|
signal.addEventListener("abort", abort, { once: true });
|
|
11
14
|
if (signal.aborted)
|
|
@@ -13,19 +16,16 @@ export function abortable(operation, signal, disposeLate) {
|
|
|
13
16
|
void operation.then(async (value) => {
|
|
14
17
|
signal.removeEventListener("abort", abort);
|
|
15
18
|
if (!aborted) {
|
|
19
|
+
settled = true;
|
|
16
20
|
resolve(value);
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
19
|
-
|
|
20
|
-
await disposeLate?.(value);
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
reject(error);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
reject(signal.reason);
|
|
23
|
+
void Promise.resolve(disposeLate?.(value)).catch(() => undefined);
|
|
27
24
|
}, (error) => {
|
|
28
25
|
signal.removeEventListener("abort", abort);
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
settled = true;
|
|
29
29
|
reject(aborted ? signal.reason : error);
|
|
30
30
|
});
|
|
31
31
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TellResult } from "./body.js";
|
|
2
2
|
import { type KillEvidence } from "./heart/index.js";
|
|
3
3
|
import { type AkuId } from "./identity.js";
|
|
4
|
-
import { type ActivityHistory } from "./projection.js";
|
|
4
|
+
import { type ActivityHistory, type ExactHistory } from "./projection.js";
|
|
5
5
|
import type { AkumaCallExecution, AkumaStatus, ForkReceipt, InterruptReceipt } from "./akuma.js";
|
|
6
6
|
import type { WorldRoot } from "../world.js";
|
|
7
7
|
declare const CALL_EXECUTION: unique symbol;
|
|
@@ -11,12 +11,14 @@ export declare class AkumaHandle {
|
|
|
11
11
|
readonly [CALL_EXECUTION]?: AkumaCallExecution;
|
|
12
12
|
constructor(id: AkuId, worldPath: WorldRoot, execution?: AkumaCallExecution);
|
|
13
13
|
private get paths();
|
|
14
|
+
private exactHistory;
|
|
14
15
|
status(): Promise<AkumaStatus>;
|
|
15
16
|
history(input?: Readonly<{
|
|
17
|
+
id?: string;
|
|
16
18
|
before?: number;
|
|
17
19
|
since?: number;
|
|
18
20
|
limit?: number;
|
|
19
|
-
}>): Promise<ActivityHistory>;
|
|
21
|
+
}>): Promise<ActivityHistory | ExactHistory>;
|
|
20
22
|
wait(predicate?: (status: AkumaStatus) => boolean, options?: Readonly<{
|
|
21
23
|
timeoutMs?: number;
|
|
22
24
|
}>): Promise<AkumaStatus>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { CONTROL_RESPONSE_MS, wakeRecordedTell } from "./body.js";
|
|
3
3
|
import { HeldAkumaLeash, activitySlice, readForkPoint, readHeart, readKill, readLastAnsweredTurn, readSoul, recordTell, requestPause, requestStop, } from "./heart/index.js";
|
|
4
|
-
import { pathsForAkuId } from "./identity.js";
|
|
5
|
-
import { projectTurns, selectHistory } from "./projection.js";
|
|
4
|
+
import { parsePublicHistoryId, pathsForAkuId } from "./identity.js";
|
|
5
|
+
import { projectTurns, selectHistory, selectExactHistory, } from "./projection.js";
|
|
6
6
|
import { resolveProviderExecution } from "./providers/index.js";
|
|
7
7
|
import { publishAkuma } from "./publication.js";
|
|
8
8
|
import { spawnAkumaBody } from "./body.js";
|
|
@@ -83,10 +83,22 @@ export class AkumaHandle {
|
|
|
83
83
|
get paths() {
|
|
84
84
|
return pathsForAkuId(this.worldPath, this.id);
|
|
85
85
|
}
|
|
86
|
+
async exactHistory(input) {
|
|
87
|
+
if (typeof input.id !== "string" || input.id.trim() === "")
|
|
88
|
+
throw new TypeError("Akuma history id must be a nonblank string");
|
|
89
|
+
if (input.before !== undefined || input.since !== undefined || input.limit !== undefined)
|
|
90
|
+
throw new TypeError("Akuma history id cannot be combined with before, since, or limit");
|
|
91
|
+
if (parsePublicHistoryId(input.id) === null)
|
|
92
|
+
throw new TypeError("Akuma history id must match turn/<positive safe integer>");
|
|
93
|
+
const slice = await activitySlice(this.paths);
|
|
94
|
+
return selectExactHistory(projectTurns(slice.rows, { lowestRetained: slice.lowestRetained, highest: slice.highest }).rows, input.id);
|
|
95
|
+
}
|
|
86
96
|
async status() {
|
|
87
97
|
return (await bornStatus(this.paths, this.id, { aperture: "monitoring" })).status;
|
|
88
98
|
}
|
|
89
99
|
async history(input = {}) {
|
|
100
|
+
if (input.id !== undefined)
|
|
101
|
+
return this.exactHistory(input);
|
|
90
102
|
if (input.before !== undefined && input.since !== undefined) {
|
|
91
103
|
throw new TypeError("Akuma history before and since are mutually exclusive");
|
|
92
104
|
}
|
|
@@ -42,11 +42,17 @@ export declare class Akuma {
|
|
|
42
42
|
listArchetypes(): Promise<readonly string[]>;
|
|
43
43
|
call(input: AkumaCallLaunchInput): Promise<AkumaHandle>;
|
|
44
44
|
beginCall(input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaBornCall>;
|
|
45
|
-
finishCall(born: AkumaBornCall
|
|
45
|
+
finishCall(born: AkumaBornCall, completion?: Readonly<{
|
|
46
|
+
participantName?: string;
|
|
47
|
+
contractId?: string;
|
|
48
|
+
}>): Promise<AkumaHandle>;
|
|
46
49
|
[CALL_WITH_CONTEXT](input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaHandle>;
|
|
47
50
|
list(input?: AkumaListInput): Promise<AkumaList>;
|
|
48
51
|
}
|
|
49
52
|
export declare function callAkumaWithContext(akuma: Akuma, input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaHandle>;
|
|
50
53
|
export declare function beginAkumaCall(akuma: Akuma, input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaBornCall>;
|
|
51
|
-
export declare function finishAkumaCall(akuma: Akuma, born: AkumaBornCall
|
|
54
|
+
export declare function finishAkumaCall(akuma: Akuma, born: AkumaBornCall, completion?: Readonly<{
|
|
55
|
+
participantName?: string;
|
|
56
|
+
contractId?: string;
|
|
57
|
+
}>): Promise<AkumaHandle>;
|
|
52
58
|
export {};
|
|
@@ -112,13 +112,18 @@ export class Akuma {
|
|
|
112
112
|
},
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
-
async finishCall(born) {
|
|
115
|
+
async finishCall(born, completion = {}) {
|
|
116
116
|
if (born.kind === "requested") {
|
|
117
117
|
return new AkumaHandle(born.id, this.path, { cwd: born.cwd, source: born.execution.source });
|
|
118
118
|
}
|
|
119
119
|
const published = await launchAkuma({
|
|
120
120
|
allocated: born.allocated,
|
|
121
|
-
launch: async (allocated) => await spawnAkumaBody({
|
|
121
|
+
launch: async (allocated) => await spawnAkumaBody({
|
|
122
|
+
paths: allocated.paths,
|
|
123
|
+
seed: born.seed,
|
|
124
|
+
initialBody: born.initialBody,
|
|
125
|
+
...(Object.keys(completion).length === 0 ? {} : { completion }),
|
|
126
|
+
}),
|
|
122
127
|
});
|
|
123
128
|
return new AkumaHandle(published.id, this.path, {
|
|
124
129
|
cwd: born.execution.cwd,
|
|
@@ -174,6 +179,6 @@ export async function callAkumaWithContext(akuma, input, context) {
|
|
|
174
179
|
export async function beginAkumaCall(akuma, input, context) {
|
|
175
180
|
return await akuma.beginCall(input, context);
|
|
176
181
|
}
|
|
177
|
-
export async function finishAkumaCall(akuma, born) {
|
|
178
|
-
return await akuma.finishCall(born);
|
|
182
|
+
export async function finishAkumaCall(akuma, born, completion = {}) {
|
|
183
|
+
return await akuma.finishCall(born, completion);
|
|
179
184
|
}
|
|
@@ -11,6 +11,10 @@ export type BodyLaunch = Readonly<{
|
|
|
11
11
|
birthSession?: Omit<SessionFact, "sequence">;
|
|
12
12
|
initialBody?: string;
|
|
13
13
|
refuseIfHeld?: boolean;
|
|
14
|
+
completion?: Readonly<{
|
|
15
|
+
participantName?: string;
|
|
16
|
+
contractId?: string;
|
|
17
|
+
}>;
|
|
14
18
|
}>;
|
|
15
19
|
export type TellWake = Readonly<{
|
|
16
20
|
kind: "told";
|
package/build/src/akuma/body.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { appendFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { abortableDelay } from "./abort.js";
|
|
5
4
|
import { BodySupervisor } from "./body-supervisor.js";
|
|
6
5
|
import { driveTurn, turnRecipe } from "./turn-drive.js";
|
|
7
6
|
import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, watchHeart, } from "./heart/index.js";
|
|
8
7
|
import { worldRootForAkumaPaths } from "./identity.js";
|
|
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";
|
|
@@ -78,29 +78,30 @@ async function persistTurn(paths, turnSequence, result, completedAt) {
|
|
|
78
78
|
await endTurn(paths, { turnSequence, outcome: { kind: "failed", diagnostic }, completedAt });
|
|
79
79
|
return { outcome: "failed", diagnostic };
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
function completionMessage(identity, completion) {
|
|
82
|
+
const participant = completion?.participantName;
|
|
83
|
+
const contract = completion?.contractId;
|
|
84
|
+
const header = [identity, participant === undefined ? undefined : `(@${participant})`, contract]
|
|
85
|
+
.filter((value) => value !== undefined)
|
|
86
|
+
.join(" ");
|
|
87
|
+
return `${header}\n✓ came back`;
|
|
87
88
|
}
|
|
88
89
|
function boundedDiagnostic(error) {
|
|
89
90
|
const text = diagnostic(error);
|
|
90
91
|
return text.length <= 500 ? text : `${text.slice(0, 500)}...`;
|
|
91
92
|
}
|
|
92
|
-
async function expressInitialOutcome(launch
|
|
93
|
+
async function expressInitialOutcome(launch) {
|
|
93
94
|
try {
|
|
94
95
|
const { Square } = await import("@astrosheep/square");
|
|
95
96
|
const identity = launch.seed?.id ?? (await readHeart(launch.paths)).soul?.id;
|
|
96
97
|
if (identity === undefined)
|
|
97
98
|
return;
|
|
98
|
-
const square = await Square.at({ path:
|
|
99
|
+
const square = await Square.at({ path: keiyakuSquarePath(worldRootForAkumaPaths(launch.paths)) });
|
|
99
100
|
try {
|
|
100
101
|
const joined = await square.implicitJoin(identity);
|
|
101
102
|
if (joined.state === "done" || joined.participant === undefined)
|
|
102
103
|
return;
|
|
103
|
-
await joined.participant.express(
|
|
104
|
+
await joined.participant.express(completionMessage(identity, launch.completion));
|
|
104
105
|
}
|
|
105
106
|
finally {
|
|
106
107
|
await square.close();
|
|
@@ -217,7 +218,7 @@ async function runBodyTurns(input) {
|
|
|
217
218
|
}
|
|
218
219
|
const outcome = await persistTurn(launch.paths, result.turnSequence, result, runtime.now());
|
|
219
220
|
if (initial !== undefined)
|
|
220
|
-
await expressInitialOutcome(launch
|
|
221
|
+
await expressInitialOutcome(launch);
|
|
221
222
|
if (outcome.outcome === "failed") {
|
|
222
223
|
await breakBody(launch.paths, { sequence: bodySequence, end: "broke-off", at: runtime.now() });
|
|
223
224
|
return;
|
|
@@ -344,8 +345,14 @@ async function awaitWake(paths, tellId, after, child, changed) {
|
|
|
344
345
|
if (winner.kind === "exited") {
|
|
345
346
|
return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
|
|
346
347
|
}
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
356
|
return winner.kind === "observer-failed"
|
|
350
357
|
? { kind: "failed", diagnostic: diagnostic(winner.error) }
|
|
351
358
|
: { kind: "failed", diagnostic: "Heart observer closed" };
|
|
@@ -355,11 +362,11 @@ async function wakePendingTells(paths, tellId, runtime) {
|
|
|
355
362
|
const watching = new AbortController();
|
|
356
363
|
let changed;
|
|
357
364
|
try {
|
|
365
|
+
changed = await runtime.observeHeart(paths, watching.signal);
|
|
358
366
|
const beforeHeart = await readHeart(paths);
|
|
359
367
|
if (tellId === undefined && beforeHeart.pending.length === 0)
|
|
360
368
|
return null;
|
|
361
369
|
const before = beforeHeart.latestBody?.sequence ?? 0;
|
|
362
|
-
changed = await runtime.observeHeart(paths, watching.signal);
|
|
363
370
|
return await awaitWake(paths, tellId, before, await runtime.spawn(paths), changed);
|
|
364
371
|
}
|
|
365
372
|
catch (error) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { parsePublicHistoryId } from "../identity.js";
|
|
1
2
|
import { answeredTurnFact, endBodyFact, finishBodyFact, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertPauseControl, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, latestKillFact, latestSessionFact, lastAnsweredTurnFact, pauseFact, sessionFactForCoordinate, stopFact, } from "./rows.js";
|
|
2
3
|
import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingTellFacts, tellFact, tellIdsForFence, } from "./tells.js";
|
|
3
4
|
import { activityFactSlice, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
|
|
4
|
-
import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart } from "./storage.js";
|
|
5
|
+
import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart, withReadOnlyHeart, } from "./storage.js";
|
|
5
6
|
import { soulFact } from "./soul.js";
|
|
6
7
|
export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
|
|
7
8
|
export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
|
|
@@ -162,7 +163,7 @@ export async function finishBodyIfIdle(paths, input) {
|
|
|
162
163
|
}
|
|
163
164
|
export async function readHeart(paths) {
|
|
164
165
|
try {
|
|
165
|
-
return await
|
|
166
|
+
return await withReadOnlyHeart(paths, (heart) => readTransaction(heart, () => ({
|
|
166
167
|
soul: soulFact(heart),
|
|
167
168
|
latestBody: latestBodyFact(heart),
|
|
168
169
|
latestSession: latestSessionFact(heart),
|
|
@@ -193,9 +194,12 @@ export async function readLastAnsweredTurn(paths) {
|
|
|
193
194
|
return await withHeart(paths, lastAnsweredTurnFact);
|
|
194
195
|
}
|
|
195
196
|
export async function readForkPoint(paths, historyId) {
|
|
197
|
+
const turnSequence = parsePublicHistoryId(historyId);
|
|
198
|
+
if (turnSequence === null)
|
|
199
|
+
return null;
|
|
196
200
|
try {
|
|
197
201
|
return await withHeart(paths, (heart) => {
|
|
198
|
-
const turn = answeredTurnFact(heart,
|
|
202
|
+
const turn = answeredTurnFact(heart, turnSequence);
|
|
199
203
|
const outcome = turn?.end?.outcome;
|
|
200
204
|
if (outcome?.kind !== "answered" || outcome.historyId === undefined)
|
|
201
205
|
return null;
|
|
@@ -116,4 +116,4 @@ export declare function sessionFactForCoordinate(database: DatabaseSync, coordin
|
|
|
116
116
|
export declare function latestKillFact(database: DatabaseSync): KillFact | null;
|
|
117
117
|
export declare function killFactForBody(database: DatabaseSync, bodySequence: number): KillFact | null;
|
|
118
118
|
export declare function lastAnsweredTurnFact(database: DatabaseSync): TurnFact | null;
|
|
119
|
-
export declare function answeredTurnFact(database: DatabaseSync,
|
|
119
|
+
export declare function answeredTurnFact(database: DatabaseSync, turnSequence: number): TurnFact | null;
|
|
@@ -263,10 +263,10 @@ export function lastAnsweredTurnFact(database) {
|
|
|
263
263
|
.get();
|
|
264
264
|
return row === undefined ? null : decodeTurnRow(row);
|
|
265
265
|
}
|
|
266
|
-
export function answeredTurnFact(database,
|
|
266
|
+
export function answeredTurnFact(database, turnSequence) {
|
|
267
267
|
const row = database
|
|
268
268
|
.prepare(`SELECT sequence, body_sequence, started_at, end_sequence, outcome, history_id,
|
|
269
|
-
session_json, answer, diagnostic, completed_at FROM turns WHERE outcome = 'answered' AND
|
|
270
|
-
.get(
|
|
269
|
+
session_json, answer, diagnostic, completed_at FROM turns WHERE outcome = 'answered' AND sequence = ?`)
|
|
270
|
+
.get(turnSequence);
|
|
271
271
|
return row === undefined ? null : decodeTurnRow(row);
|
|
272
272
|
}
|
|
@@ -10,6 +10,7 @@ export declare class HeartAbsentError extends Error {
|
|
|
10
10
|
export declare function isHeartAbsent(error: unknown): error is HeartAbsentError;
|
|
11
11
|
export declare function classifyHeartSchema(paths: AkumaPaths): Promise<"current" | "unsupported">;
|
|
12
12
|
export declare function withHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
|
|
13
|
+
export declare function withReadOnlyHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
|
|
13
14
|
export declare function transaction<T>(database: DatabaseSync, body: () => T): T;
|
|
14
15
|
export declare function readTransaction<T>(database: DatabaseSync, body: () => T): T;
|
|
15
16
|
export declare function initializeHeart(paths: AkumaPaths): Promise<void>;
|
|
@@ -12,7 +12,7 @@ const ACTIVITY_LIMIT = 5_000;
|
|
|
12
12
|
const SQLITE_CANTOPEN = 14;
|
|
13
13
|
export async function watchHeart(paths, signal) {
|
|
14
14
|
const heart = basename(paths.heart);
|
|
15
|
-
|
|
15
|
+
let changed = false;
|
|
16
16
|
let failure;
|
|
17
17
|
let wake;
|
|
18
18
|
// fs.watch opens the directory synchronously. Its throw is therefore the
|
|
@@ -20,7 +20,7 @@ export async function watchHeart(paths, signal) {
|
|
|
20
20
|
const watcher = watchDirectory(dirname(paths.heart), { signal }, (_event, filename) => {
|
|
21
21
|
const name = filename?.toString();
|
|
22
22
|
if (name === undefined || name === heart || name === `${heart}-wal`) {
|
|
23
|
-
|
|
23
|
+
changed = true;
|
|
24
24
|
wake?.();
|
|
25
25
|
}
|
|
26
26
|
});
|
|
@@ -36,8 +36,8 @@ export async function watchHeart(paths, signal) {
|
|
|
36
36
|
return (async function* () {
|
|
37
37
|
try {
|
|
38
38
|
for (;;) {
|
|
39
|
-
if (
|
|
40
|
-
|
|
39
|
+
if (changed) {
|
|
40
|
+
changed = false;
|
|
41
41
|
yield;
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
@@ -82,9 +82,9 @@ function isBusy(error) {
|
|
|
82
82
|
message.includes("database is locked") ||
|
|
83
83
|
message.includes("database is busy"));
|
|
84
84
|
}
|
|
85
|
-
async function openExistingDatabase(path, timeout) {
|
|
85
|
+
async function openExistingDatabase(path, timeout, mode = "rw") {
|
|
86
86
|
const uri = pathToFileURL(path);
|
|
87
|
-
uri.searchParams.set("mode",
|
|
87
|
+
uri.searchParams.set("mode", mode);
|
|
88
88
|
try {
|
|
89
89
|
return timeout === undefined ? new DatabaseSync(uri.href) : new DatabaseSync(uri.href, { timeout });
|
|
90
90
|
}
|
|
@@ -115,6 +115,19 @@ async function openHeart(path, verify = true) {
|
|
|
115
115
|
throw error;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
async function openHeartReadOnly(path, verify = true) {
|
|
119
|
+
const database = await openExistingDatabase(path, undefined, "ro");
|
|
120
|
+
try {
|
|
121
|
+
database.exec("PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000");
|
|
122
|
+
if (verify)
|
|
123
|
+
assertHeartSchemaVersion(database);
|
|
124
|
+
return database;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
database.close();
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
118
131
|
export async function classifyHeartSchema(paths) {
|
|
119
132
|
let database;
|
|
120
133
|
try {
|
|
@@ -141,6 +154,15 @@ export async function withHeart(paths, body) {
|
|
|
141
154
|
database.close();
|
|
142
155
|
}
|
|
143
156
|
}
|
|
157
|
+
export async function withReadOnlyHeart(paths, body) {
|
|
158
|
+
const database = await openHeartReadOnly(paths.heart);
|
|
159
|
+
try {
|
|
160
|
+
return body(database);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
database.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
144
166
|
export function transaction(database, body) {
|
|
145
167
|
database.exec("BEGIN IMMEDIATE");
|
|
146
168
|
try {
|