@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
|
@@ -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");
|
|
@@ -2,7 +2,6 @@ import { type AkuId } from "./identity.js";
|
|
|
2
2
|
import type { WorldRoot } from "../world.js";
|
|
3
3
|
export { Akuma, AkumaHandle, AkumaNotBornError, defaultWaitComplete } from "./akuma.js";
|
|
4
4
|
export type { AkumaCallInput } from "./akuma.js";
|
|
5
|
-
export type { ResultRoute } from "./result-route.js";
|
|
6
5
|
export { ALLOWED_ACTIONS } from "./allowed.js";
|
|
7
6
|
export type { AllowedAction, AllowedActions } from "./allowed.js";
|
|
8
7
|
export type { WorldRoot } from "../world.js";
|
|
@@ -10,7 +9,7 @@ export declare function probeBornAkuma(worldPath: WorldRoot, id: AkuId): Promise
|
|
|
10
9
|
export { AkumaArchetypeError, listArchetypeDefinitions, listArchetypes } from "./archetype.js";
|
|
11
10
|
export type { ArchetypeCatalogRow } from "./archetype.js";
|
|
12
11
|
export { AkumaBodyRequestError } from "./requests.js";
|
|
13
|
-
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";
|
|
14
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";
|
|
15
14
|
export type { AgentEvent, ToolCall, ToolEvent, ToolResult } from "./provider.js";
|
|
16
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)
|
|
@@ -118,53 +118,64 @@ function finishClaudeInput(input, terminal) {
|
|
|
118
118
|
else
|
|
119
119
|
input.close();
|
|
120
120
|
}
|
|
121
|
+
function consumeClaudeMessage(context, state, message) {
|
|
122
|
+
const { input, events, observation, progress, admit, rejectAdmission, settle, controls } = context;
|
|
123
|
+
if (state.terminal !== null)
|
|
124
|
+
return;
|
|
125
|
+
if (!state.admitted && "session_id" in message && typeof message.session_id === "string" && admit()) {
|
|
126
|
+
state.admitted = true;
|
|
127
|
+
events.emit({ type: "session", coordinate: { sessionId: message.session_id } });
|
|
128
|
+
}
|
|
129
|
+
emitClaudeMessage(message, events, observation);
|
|
130
|
+
if (message.type === "assistant" &&
|
|
131
|
+
message.parent_tool_use_id === null &&
|
|
132
|
+
typeof message.uuid === "string" &&
|
|
133
|
+
message.uuid.length > 0)
|
|
134
|
+
state.historyId = message.uuid;
|
|
135
|
+
if (message.type !== "result")
|
|
136
|
+
return;
|
|
137
|
+
if (message.subtype === "success") {
|
|
138
|
+
state.terminal = successfulResult(message, state.historyId);
|
|
139
|
+
progress.checkpoint += 1;
|
|
140
|
+
controls.flushReceipts();
|
|
141
|
+
queueMicrotask(controls.closeWhenIdle);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
state.terminal = { kind: "failed", diagnostic: message.errors.join("; ") || message.subtype };
|
|
145
|
+
input.close();
|
|
146
|
+
}
|
|
147
|
+
progress.ended = true;
|
|
148
|
+
if (!state.admitted)
|
|
149
|
+
rejectAdmission(new Error(state.terminal.kind === "failed" ? state.terminal.diagnostic : "Claude query ended before session admission"));
|
|
150
|
+
finishClaudeInput(input, state.terminal);
|
|
151
|
+
events.end();
|
|
152
|
+
settle(state.terminal);
|
|
153
|
+
controls.settleReceipts();
|
|
154
|
+
}
|
|
121
155
|
async function consumeClaudeQuery(context) {
|
|
122
|
-
const { query, input, events,
|
|
123
|
-
|
|
124
|
-
let terminal = null;
|
|
125
|
-
let historyId;
|
|
156
|
+
const { query, input, events, progress, rejectAdmission, settle, controls } = context;
|
|
157
|
+
const state = { admitted: false, terminal: null };
|
|
126
158
|
try {
|
|
127
|
-
for await (const message of query)
|
|
128
|
-
|
|
129
|
-
admitted = true;
|
|
130
|
-
events.emit({ type: "session", coordinate: { sessionId: message.session_id } });
|
|
131
|
-
admit();
|
|
132
|
-
}
|
|
133
|
-
emitClaudeMessage(message, events, observation);
|
|
134
|
-
if (message.type === "assistant" &&
|
|
135
|
-
message.parent_tool_use_id === null &&
|
|
136
|
-
typeof message.uuid === "string" &&
|
|
137
|
-
message.uuid.length > 0)
|
|
138
|
-
historyId = message.uuid;
|
|
139
|
-
if (message.type !== "result")
|
|
140
|
-
continue;
|
|
141
|
-
if (message.subtype === "success") {
|
|
142
|
-
terminal = successfulResult(message, historyId);
|
|
143
|
-
progress.checkpoint += 1;
|
|
144
|
-
controls.flushReceipts();
|
|
145
|
-
queueMicrotask(controls.closeWhenIdle);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
terminal = { kind: "failed", diagnostic: message.errors.join("; ") || message.subtype };
|
|
149
|
-
input.close();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
159
|
+
for await (const message of query)
|
|
160
|
+
consumeClaudeMessage(context, state, message);
|
|
152
161
|
progress.ended = true;
|
|
153
|
-
const result = terminal ?? { kind: "failed", diagnostic: "Claude query ended without a result" };
|
|
154
|
-
if (!admitted)
|
|
162
|
+
const result = state.terminal ?? { kind: "failed", diagnostic: "Claude query ended without a result" };
|
|
163
|
+
if (!state.admitted)
|
|
155
164
|
rejectAdmission(new Error(result.kind === "failed" ? result.diagnostic : "Claude query ended before session admission"));
|
|
156
165
|
settle(result);
|
|
157
166
|
}
|
|
158
167
|
catch (error) {
|
|
159
168
|
progress.ended = true;
|
|
169
|
+
if (state.terminal !== null)
|
|
170
|
+
return;
|
|
160
171
|
input.fail(error);
|
|
161
172
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
162
|
-
if (!admitted)
|
|
173
|
+
if (!state.admitted)
|
|
163
174
|
rejectAdmission(failure);
|
|
164
175
|
settle({ kind: "failed", diagnostic: failure.message });
|
|
165
176
|
}
|
|
166
177
|
finally {
|
|
167
|
-
finishClaudeInput(input, terminal);
|
|
178
|
+
finishClaudeInput(input, state.terminal);
|
|
168
179
|
events.end();
|
|
169
180
|
controls.settleReceipts();
|
|
170
181
|
}
|
|
@@ -206,9 +217,21 @@ function observeClaudeQuery(context) {
|
|
|
206
217
|
const observation = { tools: new Map() };
|
|
207
218
|
let admit;
|
|
208
219
|
let rejectAdmission;
|
|
220
|
+
let admissionOpen = true;
|
|
209
221
|
const admission = new Promise((resolve, reject) => {
|
|
210
|
-
admit =
|
|
211
|
-
|
|
222
|
+
admit = () => {
|
|
223
|
+
if (!admissionOpen)
|
|
224
|
+
return false;
|
|
225
|
+
admissionOpen = false;
|
|
226
|
+
resolve();
|
|
227
|
+
return true;
|
|
228
|
+
};
|
|
229
|
+
rejectAdmission = (error) => {
|
|
230
|
+
if (!admissionOpen)
|
|
231
|
+
return;
|
|
232
|
+
admissionOpen = false;
|
|
233
|
+
reject(error);
|
|
234
|
+
};
|
|
212
235
|
});
|
|
213
236
|
let settle;
|
|
214
237
|
const completion = new Promise((resolve) => {
|
|
@@ -247,6 +270,7 @@ function observeClaudeQuery(context) {
|
|
|
247
270
|
});
|
|
248
271
|
return {
|
|
249
272
|
admission,
|
|
273
|
+
rejectAdmission,
|
|
250
274
|
completion,
|
|
251
275
|
get ended() {
|
|
252
276
|
return progress.ended;
|
|
@@ -282,9 +306,13 @@ async function driveClaude(load, execution, drive) {
|
|
|
282
306
|
input.fail(error);
|
|
283
307
|
query.close();
|
|
284
308
|
};
|
|
285
|
-
|
|
309
|
+
let observed;
|
|
310
|
+
const abortSetup = () => {
|
|
311
|
+
shutDown(signal.reason);
|
|
312
|
+
observed?.rejectAdmission(signal.reason);
|
|
313
|
+
};
|
|
286
314
|
signal.addEventListener("abort", abortSetup, { once: true });
|
|
287
|
-
|
|
315
|
+
observed = observeClaudeQuery({
|
|
288
316
|
query,
|
|
289
317
|
input,
|
|
290
318
|
events,
|
|
@@ -296,8 +324,11 @@ async function driveClaude(load, execution, drive) {
|
|
|
296
324
|
},
|
|
297
325
|
},
|
|
298
326
|
});
|
|
327
|
+
if (signal.aborted)
|
|
328
|
+
abortSetup();
|
|
299
329
|
try {
|
|
300
330
|
await Promise.all([launchAcknowledged, observed.admission]);
|
|
331
|
+
signal.throwIfAborted();
|
|
301
332
|
}
|
|
302
333
|
finally {
|
|
303
334
|
signal.removeEventListener("abort", abortSetup);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { type AllocatedAkuma } from "./identity.js";
|
|
2
2
|
import type { OwnedProcess } from "../runtime/proc/run.js";
|
|
3
3
|
export declare const BIRTH_TIMEOUT_MS = 30000;
|
|
4
|
-
export
|
|
4
|
+
export type BirthInput = Readonly<{
|
|
5
5
|
worldPath: string;
|
|
6
6
|
archetype: string;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}>;
|
|
9
|
+
export type LaunchInput = Readonly<{
|
|
10
|
+
allocated: AllocatedAkuma;
|
|
7
11
|
awaitAsleep?: boolean;
|
|
8
|
-
reserve?(allocated: AllocatedAkuma): Promise<void>;
|
|
9
12
|
launch(allocated: AllocatedAkuma): Promise<OwnedProcess | void>;
|
|
10
13
|
signal?: AbortSignal;
|
|
11
|
-
}
|
|
14
|
+
}>;
|
|
15
|
+
export declare function birthAkuma(input: BirthInput): Promise<AllocatedAkuma>;
|
|
16
|
+
export declare function launchAkuma(input: LaunchInput): Promise<AllocatedAkuma>;
|
|
17
|
+
export declare function publishAkuma(input: BirthInput & Omit<LaunchInput, "allocated">): Promise<AllocatedAkuma>;
|
|
@@ -93,6 +93,9 @@ async function awaitBirth(paths, owned, signal) {
|
|
|
93
93
|
const settled = await settleTimedOutBirth(paths);
|
|
94
94
|
if (settled !== null)
|
|
95
95
|
return settled;
|
|
96
|
+
if (owned !== undefined)
|
|
97
|
+
await owned.terminate(true);
|
|
98
|
+
throw new Error("Akuma birth timed out before Soul admission");
|
|
96
99
|
}
|
|
97
100
|
if (owned === undefined) {
|
|
98
101
|
await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal);
|
|
@@ -156,15 +159,25 @@ async function sealLocalFailure(allocated, error) {
|
|
|
156
159
|
/* the original local publication failure remains authoritative */
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
|
-
export async function
|
|
162
|
+
export async function birthAkuma(input) {
|
|
160
163
|
input.signal?.throwIfAborted();
|
|
161
164
|
const allocated = await allocateAkumaDirectory({ worldRoot: input.worldPath, archetype: input.archetype });
|
|
162
165
|
try {
|
|
163
166
|
await initializeHeart(allocated.paths);
|
|
164
167
|
input.signal?.throwIfAborted();
|
|
165
|
-
|
|
168
|
+
return allocated;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
await sealLocalFailure(allocated, error);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function launchAkuma(input) {
|
|
176
|
+
const { allocated } = input;
|
|
177
|
+
let owned;
|
|
178
|
+
try {
|
|
166
179
|
input.signal?.throwIfAborted();
|
|
167
|
-
|
|
180
|
+
owned = await input.launch(allocated);
|
|
168
181
|
try {
|
|
169
182
|
input.signal?.throwIfAborted();
|
|
170
183
|
const soul = await awaitBirth(allocated.paths, owned ?? undefined, input.signal);
|
|
@@ -183,3 +196,7 @@ export async function publishAkuma(input) {
|
|
|
183
196
|
throw error;
|
|
184
197
|
}
|
|
185
198
|
}
|
|
199
|
+
export async function publishAkuma(input) {
|
|
200
|
+
const allocated = await birthAkuma(input);
|
|
201
|
+
return await launchAkuma({ ...input, allocated });
|
|
202
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Soul } from "./heart/index.js";
|
|
2
|
+
import { type AkumaPaths } from "./identity.js";
|
|
3
|
+
import { type StructuralRequestClaim } from "./request-wire.js";
|
|
4
|
+
import type { OwnedProcess } from "../runtime/proc/run.js";
|
|
5
|
+
export type RequestChildLaunch = Readonly<{
|
|
6
|
+
paths: AkumaPaths;
|
|
7
|
+
seed: Omit<Soul, "createdAt">;
|
|
8
|
+
initialBody: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type PumpInput = Readonly<{
|
|
11
|
+
paths: AkumaPaths;
|
|
12
|
+
parent: Soul;
|
|
13
|
+
bodySequence: number;
|
|
14
|
+
now(): string;
|
|
15
|
+
spawn(launch: RequestChildLaunch): Promise<OwnedProcess | void>;
|
|
16
|
+
upstream?: unknown;
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
}>;
|
|
19
|
+
type ServeClaim = (input: PumpInput & Readonly<{
|
|
20
|
+
directory: string;
|
|
21
|
+
transportId: string;
|
|
22
|
+
claim: StructuralRequestClaim;
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
admissionOpen(): boolean;
|
|
25
|
+
}>) => Promise<void>;
|
|
26
|
+
export declare class BodyRequestPump {
|
|
27
|
+
private readonly input;
|
|
28
|
+
private readonly serveClaim;
|
|
29
|
+
readonly directory: string;
|
|
30
|
+
readonly failure: Promise<never>;
|
|
31
|
+
private readonly closeSignal;
|
|
32
|
+
private readonly executionSignal;
|
|
33
|
+
private readonly handled;
|
|
34
|
+
private readonly running;
|
|
35
|
+
private readonly cancelBody;
|
|
36
|
+
private admissionClosed;
|
|
37
|
+
protected constructor(input: PumpInput, serveClaim: ServeClaim);
|
|
38
|
+
static openWithService(input: PumpInput, serveClaim: ServeClaim): Promise<BodyRequestPump>;
|
|
39
|
+
private run;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
stopAdmission(): void;
|
|
42
|
+
}
|
|
43
|
+
export declare function clearBodyRequestTransport(paths: AkumaPaths): Promise<void>;
|
|
44
|
+
export declare function settleBodyRequests(paths: AkumaPaths, parent: Soul, now: () => string, signal?: AbortSignal): Promise<"settled" | "pending">;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { abortableDelay } from "./abort.js";
|
|
4
|
+
import { HeldAkumaLeash, readNonterminalRequests, readSoul, serveRequest, voidRequest, } from "./heart/index.js";
|
|
5
|
+
import { pathsForAkuId } from "./identity.js";
|
|
6
|
+
import { BIRTH_TIMEOUT_MS } from "./publication.js";
|
|
7
|
+
import { atomicJson, decodeClaim, receiptPath } from "./request-wire.js";
|
|
8
|
+
const POLL_MS = 100;
|
|
9
|
+
export class BodyRequestPump {
|
|
10
|
+
input;
|
|
11
|
+
serveClaim;
|
|
12
|
+
directory;
|
|
13
|
+
failure;
|
|
14
|
+
closeSignal = new AbortController();
|
|
15
|
+
executionSignal = new AbortController();
|
|
16
|
+
handled = new Set();
|
|
17
|
+
running;
|
|
18
|
+
cancelBody;
|
|
19
|
+
admissionClosed = false;
|
|
20
|
+
constructor(input, serveClaim) {
|
|
21
|
+
this.input = input;
|
|
22
|
+
this.serveClaim = serveClaim;
|
|
23
|
+
this.directory = join(input.paths.directory, "requests", String(input.bodySequence));
|
|
24
|
+
this.cancelBody = () => {
|
|
25
|
+
this.stopAdmission();
|
|
26
|
+
if (!this.executionSignal.signal.aborted)
|
|
27
|
+
this.executionSignal.abort(input.signal.reason);
|
|
28
|
+
};
|
|
29
|
+
input.signal.addEventListener("abort", this.cancelBody, { once: true });
|
|
30
|
+
if (input.signal.aborted)
|
|
31
|
+
this.cancelBody();
|
|
32
|
+
this.running = this.run();
|
|
33
|
+
this.failure = this.running.then(() => new Promise(() => { }), (error) => Promise.reject(error));
|
|
34
|
+
}
|
|
35
|
+
static async openWithService(input, serveClaim) {
|
|
36
|
+
await mkdir(join(input.paths.directory, "requests", String(input.bodySequence)), { recursive: true });
|
|
37
|
+
return new BodyRequestPump(input, serveClaim);
|
|
38
|
+
}
|
|
39
|
+
async run() {
|
|
40
|
+
while (!this.closeSignal.signal.aborted) {
|
|
41
|
+
for (const name of await requestFiles(this.directory)) {
|
|
42
|
+
if (this.closeSignal.signal.aborted)
|
|
43
|
+
return;
|
|
44
|
+
const transportId = name.slice(0, -".request.json".length);
|
|
45
|
+
if (this.handled.has(transportId))
|
|
46
|
+
continue;
|
|
47
|
+
const path = join(this.directory, name);
|
|
48
|
+
const claim = decodeClaim(await readFile(path, "utf8"), transportId);
|
|
49
|
+
if (claim === null) {
|
|
50
|
+
await rm(path, { force: true });
|
|
51
|
+
this.handled.add(transportId);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!this.admissionClosed) {
|
|
55
|
+
try {
|
|
56
|
+
await this.serveClaim({
|
|
57
|
+
directory: this.directory,
|
|
58
|
+
transportId,
|
|
59
|
+
claim,
|
|
60
|
+
...this.input,
|
|
61
|
+
signal: this.executionSignal.signal,
|
|
62
|
+
admissionOpen: () => !this.admissionClosed,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
await projectTransportFailure(this.directory, transportId, claim, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.handled.add(transportId);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await abortableDelay(POLL_MS, this.closeSignal.signal);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (this.closeSignal.signal.aborted)
|
|
76
|
+
return;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async close() {
|
|
82
|
+
this.stopAdmission();
|
|
83
|
+
try {
|
|
84
|
+
await this.running;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.input.signal.removeEventListener("abort", this.cancelBody);
|
|
88
|
+
await rm(this.directory, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
stopAdmission() {
|
|
92
|
+
this.admissionClosed = true;
|
|
93
|
+
if (!this.closeSignal.signal.aborted)
|
|
94
|
+
this.closeSignal.abort(new Error("Body request pump closed"));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function clearBodyRequestTransport(paths) {
|
|
98
|
+
await rm(join(paths.directory, "requests"), { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
async function requestFiles(directory) {
|
|
101
|
+
try {
|
|
102
|
+
return (await readdir(directory)).filter((name) => name.endsWith(".request.json")).sort();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code === "ENOENT")
|
|
106
|
+
return [];
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function projectTransportFailure(directory, transportId, claim, error) {
|
|
111
|
+
await atomicJson(receiptPath(directory, transportId), {
|
|
112
|
+
id: claim.id,
|
|
113
|
+
action: claim.action,
|
|
114
|
+
state: "refused",
|
|
115
|
+
diagnostic: error instanceof Error ? error.message : String(error),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function matchingRequestOrigin(soul, parent, id) {
|
|
119
|
+
return soul.origin.kind === "request" && soul.origin.parent === parent && soul.origin.requestId === id;
|
|
120
|
+
}
|
|
121
|
+
async function settleReservedSoul(paths, parent, request, soul) {
|
|
122
|
+
await (matchingRequestOrigin(soul, parent.id, request.id)
|
|
123
|
+
? serveRequest(paths, request.id, request.child)
|
|
124
|
+
: voidRequest(paths, request.id, "reserved child origin does not match the request"));
|
|
125
|
+
}
|
|
126
|
+
async function settleReserved(paths, parent, request, now, signal) {
|
|
127
|
+
const childPaths = pathsForAkuId(request.world, request.child);
|
|
128
|
+
const deadline = performance.now() + BIRTH_TIMEOUT_MS;
|
|
129
|
+
for (;;) {
|
|
130
|
+
signal?.throwIfAborted();
|
|
131
|
+
if (!(await access(childPaths.directory).then(() => true, () => false))) {
|
|
132
|
+
await voidRequest(paths, request.id, "reserved child directory is absent");
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
const soul = await readSoul(childPaths);
|
|
136
|
+
if (soul !== null) {
|
|
137
|
+
await settleReservedSoul(paths, parent, request, soul);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const leash = await HeldAkumaLeash.try(childPaths);
|
|
141
|
+
if (leash !== null) {
|
|
142
|
+
try {
|
|
143
|
+
const settled = await readSoul(childPaths);
|
|
144
|
+
if (settled !== null)
|
|
145
|
+
await settleReservedSoul(paths, parent, request, settled);
|
|
146
|
+
else {
|
|
147
|
+
await leash.sealIfUnborn(childPaths, { evidence: "request settlement", at: now() });
|
|
148
|
+
await voidRequest(paths, request.id, "reserved child was sealed unborn");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
leash.release();
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (performance.now() >= deadline)
|
|
157
|
+
return false;
|
|
158
|
+
await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export async function settleBodyRequests(paths, parent, now, signal) {
|
|
162
|
+
let pending = false;
|
|
163
|
+
for (const request of await readNonterminalRequests(paths)) {
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
if (request.state === "admitted")
|
|
166
|
+
await voidRequest(paths, request.id, "body died before serving the request");
|
|
167
|
+
else if (request.state === "reserved" && !(await settleReserved(paths, parent, request, now, signal)))
|
|
168
|
+
pending = true;
|
|
169
|
+
}
|
|
170
|
+
return pending ? "pending" : "settled";
|
|
171
|
+
}
|