@astrosheep/keiyaku 4.4.0 → 4.5.1
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 +32 -3
- package/build/src/akuma/akuma-product.js +44 -20
- package/build/src/akuma/akuma.d.ts +0 -1
- package/build/src/akuma/body.d.ts +1 -7
- package/build/src/akuma/body.js +35 -13
- 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 -2
- 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.d.ts +9 -3
- package/build/src/akuma/publication.js +20 -3
- 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 +48 -178
- 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 +11 -2
- package/build/src/cli/commands/akuma-invoke.js +29 -7
- 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 +6 -3
- 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 +6 -0
- package/build/src/cli/square-edge.js +74 -0
- package/build/src/contract-worktree.js +18 -5
- package/build/src/library/akuma-creation.d.ts +14 -3
- package/build/src/library/akuma-creation.js +31 -18
- 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/process-exit.js +6 -1
- package/build/src/runtime/proc/run.js +6 -10
- package/build/src/runtime/proc/termination.d.ts +1 -0
- package/build/src/runtime/proc/termination.js +22 -2
- package/build/src/runtime/proc/windows-launch.exe +0 -0
- package/build/src/runtime/proc/windows-run.js +10 -7
- package/build/src/world.d.ts +2 -0
- package/build/src/world.js +4 -0
- package/package.json +8 -3
- package/build/src/akuma/result-route.d.ts +0 -2
- package/build/src/akuma/result-route.js +0 -1
|
@@ -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
|
}
|
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import { AkumaHandle } from "./akuma-handle.js";
|
|
2
2
|
import type { AkumaCallContext, AkumaCallInput, AkumaConfiguration, AkumaList, AkumaListInput } from "./akuma.js";
|
|
3
3
|
import { CALL_WITH_CONTEXT } from "./akuma-product-symbols.js";
|
|
4
|
-
import type { ResultRoute } from "./result-route.js";
|
|
5
4
|
import type { WorldRoot } from "../world.js";
|
|
6
|
-
type
|
|
7
|
-
|
|
5
|
+
import type { BodyLaunch } from "./body.js";
|
|
6
|
+
import type { AllocatedAkuma } from "./identity.js";
|
|
7
|
+
type AkumaCallRecipe = Omit<NonNullable<BodyLaunch["seed"]>, "id" | "archetype" | "cwd" | "origin">;
|
|
8
|
+
type BornExecution = Readonly<{
|
|
9
|
+
cwd: string;
|
|
10
|
+
source: "input" | "caller" | "process" | "world";
|
|
8
11
|
}>;
|
|
12
|
+
export type BornAkumaCall = Readonly<{
|
|
13
|
+
kind: "born";
|
|
14
|
+
allocated: AllocatedAkuma;
|
|
15
|
+
seed: AkumaCallRecipe & Readonly<{
|
|
16
|
+
id: AllocatedAkuma["id"];
|
|
17
|
+
archetype: string;
|
|
18
|
+
cwd: string;
|
|
19
|
+
origin: {
|
|
20
|
+
kind: "direct";
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
initialBody: string;
|
|
24
|
+
execution: BornExecution;
|
|
25
|
+
}>;
|
|
26
|
+
export type RequestedAkumaCall = Readonly<{
|
|
27
|
+
kind: "requested";
|
|
28
|
+
id: AllocatedAkuma["id"];
|
|
29
|
+
cwd: string;
|
|
30
|
+
execution: BornExecution;
|
|
31
|
+
}>;
|
|
32
|
+
export type AkumaBornCall = BornAkumaCall | RequestedAkumaCall;
|
|
33
|
+
type AkumaCallLaunchInput = AkumaCallInput;
|
|
9
34
|
export declare class Akuma {
|
|
10
35
|
private readonly path;
|
|
11
36
|
private readonly configuration;
|
|
@@ -16,8 +41,12 @@ export declare class Akuma {
|
|
|
16
41
|
}>): AkumaHandle;
|
|
17
42
|
listArchetypes(): Promise<readonly string[]>;
|
|
18
43
|
call(input: AkumaCallLaunchInput): Promise<AkumaHandle>;
|
|
44
|
+
beginCall(input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaBornCall>;
|
|
45
|
+
finishCall(born: AkumaBornCall): Promise<AkumaHandle>;
|
|
19
46
|
[CALL_WITH_CONTEXT](input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaHandle>;
|
|
20
47
|
list(input?: AkumaListInput): Promise<AkumaList>;
|
|
21
48
|
}
|
|
22
49
|
export declare function callAkumaWithContext(akuma: Akuma, input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaHandle>;
|
|
50
|
+
export declare function beginAkumaCall(akuma: Akuma, input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaBornCall>;
|
|
51
|
+
export declare function finishAkumaCall(akuma: Akuma, born: AkumaBornCall): Promise<AkumaHandle>;
|
|
23
52
|
export {};
|
|
@@ -6,7 +6,7 @@ import { CALL_WITH_CONTEXT } from "./akuma-product-symbols.js";
|
|
|
6
6
|
import { fleetListRow, readAkumaBirthCwd } from "./akuma-observe.js";
|
|
7
7
|
import { akuIdFromDirectoryName, akumaPaths, akumaRunRoot, archetypeName, parseAkuId } from "./identity.js";
|
|
8
8
|
import { loadArchetype, listArchetypes as readArchetypes } from "./archetype.js";
|
|
9
|
-
import {
|
|
9
|
+
import { birthAkuma, launchAkuma } from "./publication.js";
|
|
10
10
|
import { spawnAkumaBody } from "./body.js";
|
|
11
11
|
import { injectedBodyRequests, requestBodyCall } from "./requests.js";
|
|
12
12
|
import { decodeAllowedActions, unionAllowedActions } from "./allowed.js";
|
|
@@ -51,7 +51,7 @@ export class Akuma {
|
|
|
51
51
|
async call(input) {
|
|
52
52
|
return await this[CALL_WITH_CONTEXT](input, { initiatorCwd: process.cwd() });
|
|
53
53
|
}
|
|
54
|
-
async
|
|
54
|
+
async beginCall(input, context) {
|
|
55
55
|
const readonly = callReadonly(input.readonly);
|
|
56
56
|
const name = archetypeName(input.archetype);
|
|
57
57
|
const home = this.configuration.home === undefined ? {} : { home: this.configuration.home };
|
|
@@ -84,32 +84,50 @@ export class Akuma {
|
|
|
84
84
|
recipe: requestRecipe,
|
|
85
85
|
});
|
|
86
86
|
const bornCwd = await readAkumaBirthCwd(this.path, child);
|
|
87
|
-
return
|
|
87
|
+
return {
|
|
88
|
+
kind: "requested",
|
|
89
|
+
id: child,
|
|
90
|
+
cwd: bornCwd,
|
|
91
|
+
execution: { cwd: bornCwd, source: cwd === undefined ? "caller" : "input" },
|
|
92
|
+
};
|
|
88
93
|
}
|
|
89
94
|
const initiatorCwd = context.initiatorCwd;
|
|
90
95
|
const selectedCwd = input.cwd ?? initiatorCwd ?? this.path;
|
|
91
96
|
const cwd = input.cwd !== undefined && context?.cwdCanonical === true ? input.cwd : await canonicalBirthCwd(selectedCwd);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
const allocated = await birthAkuma({ worldPath: this.path, archetype: archetype.name });
|
|
98
|
+
return {
|
|
99
|
+
kind: "born",
|
|
100
|
+
allocated,
|
|
101
|
+
seed: {
|
|
102
|
+
id: allocated.id,
|
|
103
|
+
archetype: allocated.archetype,
|
|
104
|
+
...requestRecipe,
|
|
105
|
+
cwd,
|
|
106
|
+
origin: { kind: "direct" },
|
|
107
|
+
},
|
|
108
|
+
initialBody: input.body,
|
|
109
|
+
execution: {
|
|
110
|
+
cwd,
|
|
111
|
+
source: input.cwd !== undefined ? "input" : initiatorCwd === undefined ? "world" : "process",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async finishCall(born) {
|
|
116
|
+
if (born.kind === "requested") {
|
|
117
|
+
return new AkumaHandle(born.id, this.path, { cwd: born.cwd, source: born.execution.source });
|
|
118
|
+
}
|
|
119
|
+
const published = await launchAkuma({
|
|
120
|
+
allocated: born.allocated,
|
|
121
|
+
launch: async (allocated) => await spawnAkumaBody({ paths: allocated.paths, seed: born.seed, initialBody: born.initialBody }),
|
|
107
122
|
});
|
|
108
123
|
return new AkumaHandle(published.id, this.path, {
|
|
109
|
-
cwd,
|
|
110
|
-
source:
|
|
124
|
+
cwd: born.execution.cwd,
|
|
125
|
+
source: born.execution.source,
|
|
111
126
|
});
|
|
112
127
|
}
|
|
128
|
+
async [CALL_WITH_CONTEXT](input, context) {
|
|
129
|
+
return await this.finishCall(await this.beginCall(input, context));
|
|
130
|
+
}
|
|
113
131
|
async list(input = {}) {
|
|
114
132
|
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
115
133
|
throw new TypeError("Akuma list input must be an object");
|
|
@@ -153,3 +171,9 @@ export class Akuma {
|
|
|
153
171
|
export async function callAkumaWithContext(akuma, input, context) {
|
|
154
172
|
return await akuma[CALL_WITH_CONTEXT](input, context);
|
|
155
173
|
}
|
|
174
|
+
export async function beginAkumaCall(akuma, input, context) {
|
|
175
|
+
return await akuma.beginCall(input, context);
|
|
176
|
+
}
|
|
177
|
+
export async function finishAkumaCall(akuma, born) {
|
|
178
|
+
return await akuma.finishCall(born);
|
|
179
|
+
}
|
|
@@ -62,7 +62,6 @@ export type AkumaCallContext = Readonly<{
|
|
|
62
62
|
cwdCanonical?: true;
|
|
63
63
|
}>;
|
|
64
64
|
export type { TellResult, TellWake } from "./body.js";
|
|
65
|
-
export type { ResultRoute } from "./result-route.js";
|
|
66
65
|
export type InterruptReceipt = Readonly<{
|
|
67
66
|
kind: "unavailable";
|
|
68
67
|
evidence: "hung" | "untidy" | "unavailable";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type SessionFact, type Soul } from "./heart/index.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type AkumaPaths } from "./identity.js";
|
|
3
3
|
import type { ProviderAdapter } from "./provider.js";
|
|
4
|
-
import type { ResultRoute } from "./result-route.js";
|
|
5
4
|
import { type UpstreamExecutionPort, type RequestChildLaunch } from "./request-serve.js";
|
|
6
5
|
import { type OwnedProcess, type RunLogReference } from "../runtime/proc/run.js";
|
|
7
6
|
export declare const LEASH_HELD_EXIT = 75;
|
|
@@ -12,7 +11,6 @@ export type BodyLaunch = Readonly<{
|
|
|
12
11
|
birthSession?: Omit<SessionFact, "sequence">;
|
|
13
12
|
initialBody?: string;
|
|
14
13
|
refuseIfHeld?: boolean;
|
|
15
|
-
resultRoute?: ResultRoute;
|
|
16
14
|
}>;
|
|
17
15
|
export type TellWake = Readonly<{
|
|
18
16
|
kind: "told";
|
|
@@ -45,10 +43,6 @@ type BodyRuntime = Readonly<{
|
|
|
45
43
|
now(): string;
|
|
46
44
|
spawnChild?(launch: RequestChildLaunch): Promise<OwnedProcess>;
|
|
47
45
|
spawnBody?(launch: BodyLaunch): Promise<OwnedProcess>;
|
|
48
|
-
express?(route: ResultRoute, options: Readonly<{
|
|
49
|
-
as: string;
|
|
50
|
-
body: string;
|
|
51
|
-
}>): Promise<unknown>;
|
|
52
46
|
upstream?: UpstreamExecutionPort;
|
|
53
47
|
}>;
|
|
54
48
|
export declare function bodyProcessInput(launch: BodyLaunch, bodyModuleUrl?: string): Promise<{
|
package/build/src/akuma/body.js
CHANGED
|
@@ -4,6 +4,8 @@ import { abortableDelay } from "./abort.js";
|
|
|
4
4
|
import { BodySupervisor } from "./body-supervisor.js";
|
|
5
5
|
import { driveTurn, turnRecipe } from "./turn-drive.js";
|
|
6
6
|
import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, watchHeart, } from "./heart/index.js";
|
|
7
|
+
import { worldRootForAkumaPaths } from "./identity.js";
|
|
8
|
+
import { keiyakuSquarePath } from "../world.js";
|
|
7
9
|
import { resolveProviderExecution } from "./providers/index.js";
|
|
8
10
|
import { clearBodyRequestTransport, settleBodyRequests, } from "./request-serve.js";
|
|
9
11
|
import { spawnDetachedProcess, } from "../runtime/proc/run.js";
|
|
@@ -76,23 +78,37 @@ async function persistTurn(paths, turnSequence, result, completedAt) {
|
|
|
76
78
|
await endTurn(paths, { turnSequence, outcome: { kind: "failed", diagnostic }, completedAt });
|
|
77
79
|
return { outcome: "failed", diagnostic };
|
|
78
80
|
}
|
|
81
|
+
const OUTCOME_PREVIEW_LIMIT = 1_000;
|
|
82
|
+
function outcomePreview(identity, outcome) {
|
|
83
|
+
const text = outcome.outcome === "answered" ? outcome.answer : outcome.diagnostic;
|
|
84
|
+
if (text.length <= OUTCOME_PREVIEW_LIMIT)
|
|
85
|
+
return text;
|
|
86
|
+
return `${text.slice(0, OUTCOME_PREVIEW_LIMIT)}\n\nkeiyaku history ${identity} --last`;
|
|
87
|
+
}
|
|
79
88
|
function boundedDiagnostic(error) {
|
|
80
89
|
const text = diagnostic(error);
|
|
81
90
|
return text.length <= 500 ? text : `${text.slice(0, 500)}...`;
|
|
82
91
|
}
|
|
83
|
-
async function expressInitialOutcome(launch,
|
|
84
|
-
if (launch.resultRoute === undefined)
|
|
85
|
-
return;
|
|
92
|
+
async function expressInitialOutcome(launch, outcome) {
|
|
86
93
|
try {
|
|
87
|
-
const
|
|
88
|
-
await
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
94
|
+
const { Square } = await import("@astrosheep/square");
|
|
95
|
+
const identity = launch.seed?.id ?? (await readHeart(launch.paths)).soul?.id;
|
|
96
|
+
if (identity === undefined)
|
|
97
|
+
return;
|
|
98
|
+
const square = await Square.at({ path: keiyakuSquarePath(worldRootForAkumaPaths(launch.paths)) });
|
|
99
|
+
try {
|
|
100
|
+
const joined = await square.implicitJoin(identity);
|
|
101
|
+
if (joined.state === "done" || joined.participant === undefined)
|
|
102
|
+
return;
|
|
103
|
+
await joined.participant.express(outcomePreview(identity, outcome));
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await square.close();
|
|
107
|
+
}
|
|
92
108
|
}
|
|
93
109
|
catch (error) {
|
|
94
110
|
try {
|
|
95
|
-
await appendFile(launch.paths.log, `
|
|
111
|
+
await appendFile(launch.paths.log, `square outcome express failed: ${boundedDiagnostic(error)}\n`);
|
|
96
112
|
}
|
|
97
113
|
catch {
|
|
98
114
|
/* express loss never changes Heart truth */
|
|
@@ -201,7 +217,7 @@ async function runBodyTurns(input) {
|
|
|
201
217
|
}
|
|
202
218
|
const outcome = await persistTurn(launch.paths, result.turnSequence, result, runtime.now());
|
|
203
219
|
if (initial !== undefined)
|
|
204
|
-
await expressInitialOutcome(launch,
|
|
220
|
+
await expressInitialOutcome(launch, outcome);
|
|
205
221
|
if (outcome.outcome === "failed") {
|
|
206
222
|
await breakBody(launch.paths, { sequence: bodySequence, end: "broke-off", at: runtime.now() });
|
|
207
223
|
return;
|
|
@@ -328,8 +344,14 @@ async function awaitWake(paths, tellId, after, child, changed) {
|
|
|
328
344
|
if (winner.kind === "exited") {
|
|
329
345
|
return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
|
|
330
346
|
}
|
|
331
|
-
|
|
332
|
-
|
|
347
|
+
try {
|
|
348
|
+
await child.terminate();
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
/* Termination rejection leaves physical custody unresolved; release it deliberately. */
|
|
352
|
+
child.release();
|
|
353
|
+
return { kind: "failed", diagnostic: diagnostic(error) };
|
|
354
|
+
}
|
|
333
355
|
return winner.kind === "observer-failed"
|
|
334
356
|
? { kind: "failed", diagnostic: diagnostic(winner.error) }
|
|
335
357
|
: { kind: "failed", diagnostic: "Heart observer closed" };
|
|
@@ -339,11 +361,11 @@ async function wakePendingTells(paths, tellId, runtime) {
|
|
|
339
361
|
const watching = new AbortController();
|
|
340
362
|
let changed;
|
|
341
363
|
try {
|
|
364
|
+
changed = await runtime.observeHeart(paths, watching.signal);
|
|
342
365
|
const beforeHeart = await readHeart(paths);
|
|
343
366
|
if (tellId === undefined && beforeHeart.pending.length === 0)
|
|
344
367
|
return null;
|
|
345
368
|
const before = beforeHeart.latestBody?.sequence ?? 0;
|
|
346
|
-
changed = await runtime.observeHeart(paths, watching.signal);
|
|
347
369
|
return await awaitWake(paths, tellId, before, await runtime.spawn(paths), changed);
|
|
348
370
|
}
|
|
349
371
|
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;
|