@astrosheep/keiyaku 4.5.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/body.js +11 -5
- 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 +1 -1
- package/build/src/cli/square-edge.js +59 -33
- package/build/src/contract-worktree.js +18 -5
- 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
|
}
|
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";
|
|
@@ -95,7 +95,7 @@ async function expressInitialOutcome(launch, outcome) {
|
|
|
95
95
|
const identity = launch.seed?.id ?? (await readHeart(launch.paths)).soul?.id;
|
|
96
96
|
if (identity === undefined)
|
|
97
97
|
return;
|
|
98
|
-
const square = await Square.at({ path:
|
|
98
|
+
const square = await Square.at({ path: keiyakuSquarePath(worldRootForAkumaPaths(launch.paths)) });
|
|
99
99
|
try {
|
|
100
100
|
const joined = await square.implicitJoin(identity);
|
|
101
101
|
if (joined.state === "done" || joined.participant === undefined)
|
|
@@ -344,8 +344,14 @@ async function awaitWake(paths, tellId, after, child, changed) {
|
|
|
344
344
|
if (winner.kind === "exited") {
|
|
345
345
|
return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
|
|
346
346
|
}
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
355
|
return winner.kind === "observer-failed"
|
|
350
356
|
? { kind: "failed", diagnostic: diagnostic(winner.error) }
|
|
351
357
|
: { kind: "failed", diagnostic: "Heart observer closed" };
|
|
@@ -355,11 +361,11 @@ async function wakePendingTells(paths, tellId, runtime) {
|
|
|
355
361
|
const watching = new AbortController();
|
|
356
362
|
let changed;
|
|
357
363
|
try {
|
|
364
|
+
changed = await runtime.observeHeart(paths, watching.signal);
|
|
358
365
|
const beforeHeart = await readHeart(paths);
|
|
359
366
|
if (tellId === undefined && beforeHeart.pending.length === 0)
|
|
360
367
|
return null;
|
|
361
368
|
const before = beforeHeart.latestBody?.sequence ?? 0;
|
|
362
|
-
changed = await runtime.observeHeart(paths, watching.signal);
|
|
363
369
|
return await awaitWake(paths, tellId, before, await runtime.spawn(paths), changed);
|
|
364
370
|
}
|
|
365
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;
|
|
@@ -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 {
|
|
@@ -4,6 +4,13 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { identityCoordinate, identitySegments } from "../identity/coordinates.js";
|
|
5
5
|
import { normalizeIdentityStem } from "../identity/normalize.js";
|
|
6
6
|
const HEX8 = /^[0-9a-f]{8}$/u;
|
|
7
|
+
export function parsePublicHistoryId(value) {
|
|
8
|
+
const match = /^turn\/([1-9][0-9]*)$/u.exec(value);
|
|
9
|
+
if (match === null)
|
|
10
|
+
return null;
|
|
11
|
+
const sequence = Number(match[1]);
|
|
12
|
+
return Number.isSafeInteger(sequence) ? sequence : null;
|
|
13
|
+
}
|
|
7
14
|
export function archetypeName(value) {
|
|
8
15
|
if (value.length === 0 || normalizeIdentityStem({ source: value }) !== value) {
|
|
9
16
|
throw new TypeError("Akuma name must be one normalized human identity segment");
|
|
@@ -9,7 +9,7 @@ export declare function probeBornAkuma(worldPath: WorldRoot, id: AkuId): Promise
|
|
|
9
9
|
export { AkumaArchetypeError, listArchetypeDefinitions, listArchetypes } from "./archetype.js";
|
|
10
10
|
export type { ArchetypeCatalogRow } from "./archetype.js";
|
|
11
11
|
export { AkumaBodyRequestError } from "./requests.js";
|
|
12
|
-
export type { AkumaList, AkumaListInput, AkumaListRow, AkumaStatus, ActivityHistory, ActivityRow, ActivitySnapshot, ActivitySnapshotEntry, ForkReceipt, InterruptReceipt, TellResult, TellWake, UnbornAkumaListRow, } from "./akuma.js";
|
|
12
|
+
export type { AkumaList, AkumaListInput, AkumaListRow, AkumaStatus, ActivityHistory, ExactHistory, ActivityRow, ActivitySnapshot, ActivitySnapshotEntry, ForkReceipt, InterruptReceipt, TellResult, TellWake, UnbornAkumaListRow, } from "./akuma.js";
|
|
13
13
|
export type { ActiveToolRow, ClosedTurn, ClosedTurnRow, CompletedToolRow, HistoryCursor, HistoryPage, IdleSnapshotRow, OpenSnapshotRow, OpenTurn, OpenTurnRow, OutcomeRow, ReportedFileChange, RetainedWindow, Snapshot, SnapshotRow, TellRow, TurnLedger, TurnOutcome, TurnStartRow, UnsettledToolRow, } from "./akuma.js";
|
|
14
14
|
export type { AgentEvent, ToolCall, ToolEvent, ToolResult } from "./provider.js";
|
|
15
15
|
export type { AkuId } from "./identity.js";
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type ToolCall, type ToolResult } from "./provider.js";
|
|
2
|
-
import type { TellDelivery, TellFact, TimelineFact
|
|
3
|
-
export type TurnOutcome =
|
|
2
|
+
import type { TellDelivery, TellFact, TimelineFact } from "./heart/index.js";
|
|
3
|
+
export type TurnOutcome = Readonly<{
|
|
4
|
+
kind: "answered";
|
|
5
|
+
historyId: string;
|
|
6
|
+
answer: string;
|
|
7
|
+
}> | Readonly<{
|
|
8
|
+
kind: "failed";
|
|
9
|
+
historyId: string;
|
|
10
|
+
diagnostic: string;
|
|
11
|
+
}>;
|
|
4
12
|
export type TurnStartRow = Readonly<{
|
|
5
13
|
kind: "turn";
|
|
6
14
|
sequence: number;
|
|
@@ -153,7 +161,15 @@ export type ActivityHistory = Readonly<{
|
|
|
153
161
|
lowestRetained: number | null;
|
|
154
162
|
highest: number | null;
|
|
155
163
|
}>;
|
|
164
|
+
export type ExactHistory = Readonly<{
|
|
165
|
+
kind: "exact";
|
|
166
|
+
outcome: OutcomeRow;
|
|
167
|
+
}> | Readonly<{
|
|
168
|
+
kind: "unknown-history";
|
|
169
|
+
id: string;
|
|
170
|
+
}>;
|
|
156
171
|
export type HistoryPage = ActivityHistory;
|
|
172
|
+
export declare function selectExactHistory(rows: readonly ActivityRow[], id: string): ExactHistory;
|
|
157
173
|
export type HistoryCursor = Readonly<{
|
|
158
174
|
before?: number;
|
|
159
175
|
since?: number;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { decodeAgentEvent } from "./provider.js";
|
|
2
|
+
function publicOutcome(turnSequence, outcome) {
|
|
3
|
+
const historyId = `turn/${turnSequence}`;
|
|
4
|
+
return outcome.kind === "answered"
|
|
5
|
+
? { kind: "answered", historyId, answer: outcome.answer }
|
|
6
|
+
: { kind: "failed", historyId, diagnostic: outcome.diagnostic };
|
|
7
|
+
}
|
|
8
|
+
export function selectExactHistory(rows, id) {
|
|
9
|
+
const outcome = rows.find((row) => row.kind === "outcome" && row.outcome.historyId === id);
|
|
10
|
+
return outcome === undefined ? { kind: "unknown-history", id } : { kind: "exact", outcome };
|
|
11
|
+
}
|
|
2
12
|
function duration(startedAt, completedAt) {
|
|
3
13
|
const start = Date.parse(startedAt);
|
|
4
14
|
const end = Date.parse(completedAt);
|
|
@@ -171,7 +181,7 @@ export function projectTurns(facts, retained = {
|
|
|
171
181
|
sequence: fact.sequence,
|
|
172
182
|
turnSequence: fact.turnSequence,
|
|
173
183
|
at: fact.completedAt,
|
|
174
|
-
outcome: fact.outcome,
|
|
184
|
+
outcome: publicOutcome(fact.turnSequence, fact.outcome),
|
|
175
185
|
};
|
|
176
186
|
const turn = state.turnsBySequence.get(fact.turnSequence);
|
|
177
187
|
if (turn === undefined)
|