@astrosheep/keiyaku 4.3.0 → 4.5.0
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/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 +26 -10
- package/build/src/akuma/index.d.ts +0 -1
- package/build/src/akuma/publication.d.ts +9 -3
- package/build/src/akuma/publication.js +17 -3
- package/build/src/akuma/request-serve.js +1 -5
- package/build/src/cli/commands/akuma-invoke.d.ts +2 -2
- package/build/src/cli/commands/akuma-invoke.js +21 -2
- package/build/src/cli/invoke.js +1 -3
- package/build/src/cli/square-edge.d.ts +6 -0
- package/build/src/cli/square-edge.js +48 -0
- package/build/src/library/akuma-creation.d.ts +14 -3
- package/build/src/library/akuma-creation.js +31 -18
- 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 +16 -7
- package/package.json +2 -2
- package/build/src/akuma/result-route.d.ts +0 -2
- package/build/src/akuma/result-route.js +0 -1
|
@@ -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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { appendFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { abortableDelay } from "./abort.js";
|
|
4
5
|
import { BodySupervisor } from "./body-supervisor.js";
|
|
5
6
|
import { driveTurn, turnRecipe } from "./turn-drive.js";
|
|
6
7
|
import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, watchHeart, } from "./heart/index.js";
|
|
8
|
+
import { worldRootForAkumaPaths } from "./identity.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: join(worldRootForAkumaPaths(launch.paths), ".square", "PUBLIC.square") });
|
|
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;
|
|
@@ -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";
|
|
@@ -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>;
|
|
@@ -156,15 +156,25 @@ async function sealLocalFailure(allocated, error) {
|
|
|
156
156
|
/* the original local publication failure remains authoritative */
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
export async function
|
|
159
|
+
export async function birthAkuma(input) {
|
|
160
160
|
input.signal?.throwIfAborted();
|
|
161
161
|
const allocated = await allocateAkumaDirectory({ worldRoot: input.worldPath, archetype: input.archetype });
|
|
162
162
|
try {
|
|
163
163
|
await initializeHeart(allocated.paths);
|
|
164
164
|
input.signal?.throwIfAborted();
|
|
165
|
-
|
|
165
|
+
return allocated;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
await sealLocalFailure(allocated, error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export async function launchAkuma(input) {
|
|
173
|
+
const { allocated } = input;
|
|
174
|
+
let owned;
|
|
175
|
+
try {
|
|
166
176
|
input.signal?.throwIfAborted();
|
|
167
|
-
|
|
177
|
+
owned = await input.launch(allocated);
|
|
168
178
|
try {
|
|
169
179
|
input.signal?.throwIfAborted();
|
|
170
180
|
const soul = await awaitBirth(allocated.paths, owned ?? undefined, input.signal);
|
|
@@ -183,3 +193,7 @@ export async function publishAkuma(input) {
|
|
|
183
193
|
throw error;
|
|
184
194
|
}
|
|
185
195
|
}
|
|
196
|
+
export async function publishAkuma(input) {
|
|
197
|
+
const allocated = await birthAkuma(input);
|
|
198
|
+
return await launchAkuma({ ...input, allocated });
|
|
199
|
+
}
|
|
@@ -111,14 +111,10 @@ async function serveCall(input) {
|
|
|
111
111
|
worldPath: world,
|
|
112
112
|
archetype: request.archetype,
|
|
113
113
|
signal: input.signal,
|
|
114
|
-
reserve: async (allocated) => {
|
|
115
|
-
if (!input.admissionOpen())
|
|
116
|
-
throw new Error("body closed request admission");
|
|
117
|
-
fact = await reserveRequest(input.paths, request.id, allocated.id);
|
|
118
|
-
},
|
|
119
114
|
launch: async (allocated) => {
|
|
120
115
|
if (!input.admissionOpen())
|
|
121
116
|
throw new Error("body closed request admission");
|
|
117
|
+
fact = await reserveRequest(input.paths, request.id, allocated.id);
|
|
122
118
|
return await input.spawn({
|
|
123
119
|
paths: allocated.paths,
|
|
124
120
|
seed: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AkuId, type ActivityHistory
|
|
1
|
+
import { type AkuId, type ActivityHistory } from "../../akuma/index.js";
|
|
2
2
|
import { Keiyaku, type AkumaKillResult, type AkumaHistoryResult, type AkumaObservation, type AkumaTellResult, type AkumaWaitResult, type CallResult, type ForkResult, type Keiyaku as KeiyakuContract, type Repo } from "../../index.js";
|
|
3
3
|
import type { Settings } from "../../settings.js";
|
|
4
4
|
import type { WorldRoot } from "../../world.js";
|
|
@@ -72,7 +72,7 @@ type InvokeInput = Readonly<{
|
|
|
72
72
|
settings?: Settings;
|
|
73
73
|
contract?: KeiyakuContract;
|
|
74
74
|
repo?: Repo;
|
|
75
|
-
|
|
75
|
+
environment: NodeJS.ProcessEnv;
|
|
76
76
|
readStdin(): Promise<string>;
|
|
77
77
|
}>;
|
|
78
78
|
export declare function invokeAkuma(command: InvokedAkumaCommand, input: InvokeInput): Promise<AkumaInvocationResult>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Keiyaku, } from "../../index.js";
|
|
2
|
+
import { beginCall, finishCall } from "../../library/akuma-creation.js";
|
|
3
|
+
import { recognizeAndListen } from "../square-edge.js";
|
|
2
4
|
function inputAlias(selector) {
|
|
3
5
|
return selector.startsWith("@") ? selector : undefined;
|
|
4
6
|
}
|
|
@@ -100,7 +102,7 @@ export async function invokeAkuma(command, input) {
|
|
|
100
102
|
switch (command.command) {
|
|
101
103
|
case "call": {
|
|
102
104
|
const body = await promptBody(command, input);
|
|
103
|
-
const
|
|
105
|
+
const born = await beginCall({
|
|
104
106
|
path: input.path,
|
|
105
107
|
archetype: command.archetype,
|
|
106
108
|
body,
|
|
@@ -113,8 +115,25 @@ export async function invokeAkuma(command, input) {
|
|
|
113
115
|
...(input.contract === undefined ? {} : { contract: input.contract }),
|
|
114
116
|
...(command.alias === undefined ? {} : { alias: command.alias }),
|
|
115
117
|
...(command.allowed === undefined ? {} : { allowed: command.allowed }),
|
|
116
|
-
...(input.resultRoute === undefined ? {} : { resultRoute: input.resultRoute }),
|
|
117
118
|
});
|
|
119
|
+
const listener = born.born.kind === "born"
|
|
120
|
+
? await recognizeAndListen(input.path, input.environment, born.born.allocated)
|
|
121
|
+
: undefined;
|
|
122
|
+
let result;
|
|
123
|
+
try {
|
|
124
|
+
result = await finishCall(born);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (listener?.committed === true) {
|
|
128
|
+
try {
|
|
129
|
+
await listener.rollback();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* preserve launch failure */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
118
137
|
return { kind: "akuma", action: "call", result, world: input.path };
|
|
119
138
|
}
|
|
120
139
|
case "wait":
|
package/build/src/cli/invoke.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { captureRoute } from "@astrosheep/square";
|
|
2
1
|
import { resolveActor } from "./actor.js";
|
|
3
2
|
import { isParsedAkumaCommand } from "./commands/akuma.js";
|
|
4
3
|
import { invokeContractMutation } from "./commands/contract-invoke.js";
|
|
@@ -106,14 +105,13 @@ async function invokeAkumaFromEdge(parsed, input) {
|
|
|
106
105
|
const contract = parsed.command === "call" && parsed.contract !== undefined
|
|
107
106
|
? (await import("./selectors.js")).contractFromInput(repo, parsed.contract).contract
|
|
108
107
|
: undefined;
|
|
109
|
-
const resultRoute = parsed.command === "call" ? captureRoute({ cwd: input.invocationCwd, env: edge.environment }) : null;
|
|
110
108
|
return await invokeAkuma(parsed, {
|
|
111
109
|
path,
|
|
112
110
|
...(statedCwd === undefined ? {} : { statedCwd }),
|
|
113
111
|
...(home === undefined ? {} : { home }),
|
|
114
112
|
...(configuration === undefined ? {} : { settings: configuration }),
|
|
115
113
|
...(contract === undefined ? (repo === undefined ? {} : { repo }) : { contract }),
|
|
116
|
-
|
|
114
|
+
environment: edge.environment,
|
|
117
115
|
readStdin: edge.readStdin,
|
|
118
116
|
});
|
|
119
117
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AllocatedAkuma } from "../akuma/identity.js";
|
|
2
|
+
import type { WorldRoot } from "../world.js";
|
|
3
|
+
export declare function recognizeAndListen(worldRoot: WorldRoot, environment: NodeJS.ProcessEnv, allocated: AllocatedAkuma): Promise<{
|
|
4
|
+
committed: boolean;
|
|
5
|
+
rollback(): Promise<void>;
|
|
6
|
+
} | void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Square } from "@astrosheep/square";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export async function recognizeAndListen(worldRoot, environment, allocated) {
|
|
4
|
+
const path = join(worldRoot, ".square", "PUBLIC.square");
|
|
5
|
+
let square;
|
|
6
|
+
try {
|
|
7
|
+
square = await Square.at({ path });
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code === "ENOENT" || error.code === "unavailable")
|
|
11
|
+
return;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const recognize = square.recognize;
|
|
16
|
+
if (recognize === undefined)
|
|
17
|
+
return;
|
|
18
|
+
const participant = await recognize.call(square, environment);
|
|
19
|
+
if (participant === null)
|
|
20
|
+
return;
|
|
21
|
+
const listener = participant;
|
|
22
|
+
const change = await listener.listen(allocated.id);
|
|
23
|
+
if (change.activity === null)
|
|
24
|
+
return;
|
|
25
|
+
return {
|
|
26
|
+
committed: true,
|
|
27
|
+
rollback: async () => {
|
|
28
|
+
const rollbackSquare = await Square.at({ path });
|
|
29
|
+
try {
|
|
30
|
+
const current = rollbackSquare
|
|
31
|
+
.recognize;
|
|
32
|
+
if (current === undefined)
|
|
33
|
+
return;
|
|
34
|
+
const rollbackParticipant = await current.call(rollbackSquare, environment);
|
|
35
|
+
if (rollbackParticipant === null)
|
|
36
|
+
return;
|
|
37
|
+
await rollbackParticipant.ignore(allocated.id);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await rollbackSquare.close();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await square.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AliasBinding } from "../alias/index.js";
|
|
2
|
-
import { type AkumaStatus, type ForkReceipt, type ReadonlyRestraint
|
|
2
|
+
import { type AkumaStatus, type ForkReceipt, type ReadonlyRestraint } from "../akuma/akuma.js";
|
|
3
|
+
import { type AkumaBornCall } from "../akuma/akuma-product.js";
|
|
3
4
|
import type { AkuId } from "../akuma/identity.js";
|
|
4
5
|
import { type Dispatch, type DispatchFailure } from "../dispatch/index.js";
|
|
5
6
|
import { type AkumaAlias } from "../identity/selector.js";
|
|
@@ -48,11 +49,9 @@ export type CallInput = Readonly<{
|
|
|
48
49
|
contract?: Keiyaku;
|
|
49
50
|
alias?: AkumaAlias;
|
|
50
51
|
allowed?: readonly AllowedAction[];
|
|
51
|
-
resultRoute?: ResultRoute;
|
|
52
52
|
}>;
|
|
53
53
|
export type CallObservation = Readonly<{
|
|
54
54
|
kind: "detached";
|
|
55
|
-
resultRoute: "captured" | "not-captured";
|
|
56
55
|
}> | Readonly<{
|
|
57
56
|
kind: "observed";
|
|
58
57
|
status: AkumaStatus;
|
|
@@ -72,6 +71,16 @@ export type CallResult = Readonly<{
|
|
|
72
71
|
alias: AliasStage;
|
|
73
72
|
observation: CallObservation;
|
|
74
73
|
}>;
|
|
74
|
+
type CallExecution = CallResult["execution"];
|
|
75
|
+
export type BornCall = Readonly<{
|
|
76
|
+
path: WorldRoot;
|
|
77
|
+
born: AkumaBornCall;
|
|
78
|
+
execution: CallExecution;
|
|
79
|
+
mode: "wait" | "detach";
|
|
80
|
+
timeoutMs: number;
|
|
81
|
+
dispatch: DispatchStage;
|
|
82
|
+
alias: AliasStage;
|
|
83
|
+
}>;
|
|
75
84
|
export type ForkInput = Readonly<{
|
|
76
85
|
path: WorldRoot;
|
|
77
86
|
akuma: string;
|
|
@@ -89,5 +98,7 @@ export type ForkResult = Readonly<{
|
|
|
89
98
|
}>> & Readonly<{
|
|
90
99
|
parent: AkuId;
|
|
91
100
|
}>);
|
|
101
|
+
export declare function beginCall(input: CallInput): Promise<BornCall>;
|
|
102
|
+
export declare function finishCall(born: BornCall): Promise<CallResult>;
|
|
92
103
|
export declare function callKeiyaku(input: CallInput): Promise<CallResult>;
|
|
93
104
|
export declare function forkKeiyaku(input: ForkInput): Promise<ForkResult>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { moveAlias } from "../alias/index.js";
|
|
3
|
-
import { Akuma
|
|
3
|
+
import { Akuma } from "../akuma/akuma.js";
|
|
4
|
+
import { beginAkumaCall, finishAkumaCall } from "../akuma/akuma-product.js";
|
|
4
5
|
import { AuthorityCorruptionError } from "../core/facts/errors.js";
|
|
5
6
|
import { publishDispatch, readDispatch } from "../dispatch/index.js";
|
|
6
7
|
import { parseAkumaAlias } from "../identity/selector.js";
|
|
@@ -77,9 +78,9 @@ function callTimeout(value, mode) {
|
|
|
77
78
|
}
|
|
78
79
|
return value;
|
|
79
80
|
}
|
|
80
|
-
async function observeCall(handle, mode, timeoutMs
|
|
81
|
+
async function observeCall(handle, mode, timeoutMs) {
|
|
81
82
|
if (mode === "detach") {
|
|
82
|
-
return { kind: "detached"
|
|
83
|
+
return { kind: "detached" };
|
|
83
84
|
}
|
|
84
85
|
try {
|
|
85
86
|
return { kind: "observed", status: await handle.wait(undefined, { timeoutMs }) };
|
|
@@ -165,7 +166,7 @@ async function resolveCallExecution(input) {
|
|
|
165
166
|
}
|
|
166
167
|
return undefined;
|
|
167
168
|
}
|
|
168
|
-
export async function
|
|
169
|
+
export async function beginCall(input) {
|
|
169
170
|
const values = requireInput(input, "Keiyaku.call input");
|
|
170
171
|
onlyKeys(values, [
|
|
171
172
|
"path",
|
|
@@ -180,7 +181,6 @@ export async function callKeiyaku(input) {
|
|
|
180
181
|
"contract",
|
|
181
182
|
"alias",
|
|
182
183
|
"allowed",
|
|
183
|
-
"resultRoute",
|
|
184
184
|
], "Keiyaku.call input");
|
|
185
185
|
const path = nonblank(values.path, "path");
|
|
186
186
|
const archetype = nonblank(values.archetype, "archetype");
|
|
@@ -193,7 +193,6 @@ export async function callKeiyaku(input) {
|
|
|
193
193
|
const settings = settingsOption(values.settings);
|
|
194
194
|
const alias = values.alias === undefined ? undefined : parseAkumaAlias(nonblank(values.alias, "alias"));
|
|
195
195
|
const seat = values.contract === undefined ? undefined : seatForKeiyaku(values.contract);
|
|
196
|
-
const resultRoute = values.resultRoute;
|
|
197
196
|
const execution = await resolveCallExecution({
|
|
198
197
|
path,
|
|
199
198
|
...(cwd === undefined ? {} : { cwd }),
|
|
@@ -206,23 +205,20 @@ export async function callKeiyaku(input) {
|
|
|
206
205
|
...(readonlyRequested === undefined ? {} : { readonly: readonlyRequested }),
|
|
207
206
|
...(values.allowed === undefined ? {} : { allowed: values.allowed }),
|
|
208
207
|
...(execution === undefined ? {} : { cwd: execution.cwd }),
|
|
209
|
-
...(resultRoute === undefined ? {} : { resultRoute }),
|
|
210
208
|
};
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
throw new Error("Akuma call is missing its birth execution");
|
|
215
|
-
const readonly = (await handle.status()).readonly;
|
|
209
|
+
const born = await beginAkumaCall(world, call, execution === undefined ? {} : { cwdCanonical: true });
|
|
210
|
+
const akuma = born.kind === "requested" ? born.id : born.allocated.id;
|
|
211
|
+
const completedExecution = execution ?? born.execution;
|
|
216
212
|
const dispatch = seat === undefined
|
|
217
213
|
? { kind: "none" }
|
|
218
|
-
: await dispatchStage({ repository: seat.scope, akuId:
|
|
214
|
+
: await dispatchStage({ repository: seat.scope, akuId: akuma, contractId: seat.id });
|
|
219
215
|
let aliasStage = { kind: "none" };
|
|
220
216
|
if (alias !== undefined) {
|
|
221
217
|
if (dispatch.kind === "failed")
|
|
222
218
|
aliasStage = { kind: "skipped", reason: "dispatch-failed" };
|
|
223
219
|
else {
|
|
224
220
|
try {
|
|
225
|
-
const moved = await moveAlias({ world: path, alias, akuId:
|
|
221
|
+
const moved = await moveAlias({ world: path, alias, akuId: akuma });
|
|
226
222
|
aliasStage = { kind: "aliased", alias: moved.alias, previous: moved.previous };
|
|
227
223
|
}
|
|
228
224
|
catch (error) {
|
|
@@ -230,17 +226,34 @@ export async function callKeiyaku(input) {
|
|
|
230
226
|
}
|
|
231
227
|
}
|
|
232
228
|
}
|
|
233
|
-
const observation = await observeCall(handle, mode, timeoutMs, resultRoute);
|
|
234
229
|
return {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
...(readonly === undefined ? {} : { readonly }),
|
|
230
|
+
path,
|
|
231
|
+
born,
|
|
238
232
|
execution: completedExecution,
|
|
233
|
+
mode,
|
|
234
|
+
timeoutMs,
|
|
239
235
|
dispatch,
|
|
240
236
|
alias: aliasStage,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export async function finishCall(born) {
|
|
240
|
+
const world = akumaWorld(born.path);
|
|
241
|
+
const handle = await finishAkumaCall(world, born.born);
|
|
242
|
+
const readonly = (await handle.status()).readonly;
|
|
243
|
+
const observation = await observeCall(handle, born.mode, born.timeoutMs);
|
|
244
|
+
return {
|
|
245
|
+
kind: "called",
|
|
246
|
+
akuma: handle.id,
|
|
247
|
+
...(readonly === undefined ? {} : { readonly }),
|
|
248
|
+
execution: born.execution,
|
|
249
|
+
dispatch: born.dispatch,
|
|
250
|
+
alias: born.alias,
|
|
241
251
|
observation,
|
|
242
252
|
};
|
|
243
253
|
}
|
|
254
|
+
export async function callKeiyaku(input) {
|
|
255
|
+
return await finishCall(await beginCall(input));
|
|
256
|
+
}
|
|
244
257
|
export async function forkKeiyaku(input) {
|
|
245
258
|
const values = requireInput(input, "Keiyaku.fork input");
|
|
246
259
|
onlyKeys(values, ["path", "akuma", "at", "repo"], "Keiyaku.fork input");
|
|
@@ -5,12 +5,17 @@ export function detachedExitStatus(code, signal) {
|
|
|
5
5
|
export async function retainDetachedExitEvidence(log, path, from, code, signal) {
|
|
6
6
|
const status = detachedExitStatus(code, signal);
|
|
7
7
|
const marker = Buffer.from(`[child ${status}]\n`);
|
|
8
|
+
const before = await log.stat();
|
|
9
|
+
if (before.size < from)
|
|
10
|
+
throw new Error("run log shrank before exit evidence was retained");
|
|
8
11
|
let written = 0;
|
|
12
|
+
let position = before.size;
|
|
9
13
|
while (written < marker.byteLength) {
|
|
10
|
-
const write = await log.write(marker, written, marker.byteLength - written);
|
|
14
|
+
const write = await log.write(marker, written, marker.byteLength - written, position);
|
|
11
15
|
if (write.bytesWritten === 0)
|
|
12
16
|
throw new Error("run log exit marker write made no progress");
|
|
13
17
|
written += write.bytesWritten;
|
|
18
|
+
position += write.bytesWritten;
|
|
14
19
|
}
|
|
15
20
|
const evidence = await log.stat();
|
|
16
21
|
if (evidence.size < from)
|
|
@@ -40,13 +40,6 @@ export async function spawnDetachedProcess(input) {
|
|
|
40
40
|
return spawnWindowsRetainedProcess(input);
|
|
41
41
|
const log = await open(input.log, "a");
|
|
42
42
|
let launched = false;
|
|
43
|
-
let logClosed = false;
|
|
44
|
-
const closeLog = async () => {
|
|
45
|
-
if (logClosed)
|
|
46
|
-
return;
|
|
47
|
-
logClosed = true;
|
|
48
|
-
await log.close();
|
|
49
|
-
};
|
|
50
43
|
try {
|
|
51
44
|
const from = (await log.stat()).size;
|
|
52
45
|
const child = spawn(input.argv[0], input.argv.slice(1), spawnOptionsFor(input, ["ignore", log.fd, log.fd]));
|
|
@@ -62,14 +55,16 @@ export async function spawnDetachedProcess(input) {
|
|
|
62
55
|
const status = detachedExitStatus(code, signal);
|
|
63
56
|
let failure;
|
|
64
57
|
let result;
|
|
58
|
+
let exitLog;
|
|
65
59
|
try {
|
|
66
|
-
|
|
60
|
+
exitLog = await open(input.log, "r+");
|
|
61
|
+
result = await retainDetachedExitEvidence(exitLog, input.log, from, code, signal);
|
|
67
62
|
}
|
|
68
63
|
catch (error) {
|
|
69
64
|
failure = error;
|
|
70
65
|
}
|
|
71
66
|
try {
|
|
72
|
-
await
|
|
67
|
+
await exitLog?.close();
|
|
73
68
|
}
|
|
74
69
|
catch (error) {
|
|
75
70
|
failure ??= error;
|
|
@@ -89,6 +84,7 @@ export async function spawnDetachedProcess(input) {
|
|
|
89
84
|
if (child.pid === undefined)
|
|
90
85
|
throw new Error("detached process spawned without a pid");
|
|
91
86
|
const pid = child.pid;
|
|
87
|
+
await log.close();
|
|
92
88
|
launched = true;
|
|
93
89
|
return {
|
|
94
90
|
pid,
|
|
@@ -99,7 +95,7 @@ export async function spawnDetachedProcess(input) {
|
|
|
99
95
|
}
|
|
100
96
|
finally {
|
|
101
97
|
if (!launched)
|
|
102
|
-
await
|
|
98
|
+
await log.close();
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
101
|
function terminalOutcome(code, stdout, stderr, consuming) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
2
|
export declare function terminateWindowsTree(pid: number): Promise<void>;
|
|
3
|
+
export declare function settleWindowsTermination(exit: Promise<void>): Promise<void>;
|
|
3
4
|
export declare function terminateOwnedProcess(child: ChildProcess, force?: boolean): Promise<void>;
|
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
const TERMINATION_GRACE_MS = 250;
|
|
5
|
+
const WINDOWS_TERMINATION_SETTLE_MS = 250;
|
|
5
6
|
const WINDOWS_TERMINATION_TIMEOUT_MS = 1_000;
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
function ignoreMissingProcess(error) {
|
|
@@ -23,6 +24,20 @@ export async function terminateWindowsTree(pid) {
|
|
|
23
24
|
throw error;
|
|
24
25
|
}
|
|
25
26
|
}
|
|
27
|
+
export async function settleWindowsTermination(exit) {
|
|
28
|
+
const settled = await Promise.race([
|
|
29
|
+
exit.then(() => true),
|
|
30
|
+
delay(WINDOWS_TERMINATION_TIMEOUT_MS, false, { ref: false }).then(() => false),
|
|
31
|
+
]);
|
|
32
|
+
if (!settled)
|
|
33
|
+
throw new Error("Windows process did not exit after taskkill");
|
|
34
|
+
await delay(WINDOWS_TERMINATION_SETTLE_MS);
|
|
35
|
+
}
|
|
36
|
+
function closeWindowsStreams(child) {
|
|
37
|
+
child.stdin?.destroy();
|
|
38
|
+
child.stdout?.destroy();
|
|
39
|
+
child.stderr?.destroy();
|
|
40
|
+
}
|
|
26
41
|
export async function terminateOwnedProcess(child, force = false) {
|
|
27
42
|
const pid = child.pid;
|
|
28
43
|
if (pid === undefined || child.exitCode !== null || child.signalCode !== null)
|
|
@@ -39,8 +54,13 @@ export async function terminateOwnedProcess(child, force = false) {
|
|
|
39
54
|
});
|
|
40
55
|
});
|
|
41
56
|
if (process.platform === "win32") {
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
try {
|
|
58
|
+
await terminateWindowsTree(pid);
|
|
59
|
+
await settleWindowsTermination(exit);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
closeWindowsStreams(child);
|
|
63
|
+
}
|
|
44
64
|
return;
|
|
45
65
|
}
|
|
46
66
|
if (force) {
|
|
Binary file
|
|
@@ -3,8 +3,8 @@ import { createInterface } from "node:readline";
|
|
|
3
3
|
import { launchFailure, spawnWindowsLauncher } from "./launch.js";
|
|
4
4
|
import { detachedExitStatus, retainDetachedExitEvidence } from "./process-exit.js";
|
|
5
5
|
import { createProcessLifecycle } from "./lifecycle.js";
|
|
6
|
-
import { terminateWindowsTree } from "./termination.js";
|
|
7
|
-
function createWindowsContext(input,
|
|
6
|
+
import { settleWindowsTermination, terminateWindowsTree } from "./termination.js";
|
|
7
|
+
function createWindowsContext(input, from, closeLog) {
|
|
8
8
|
const child = spawnWindowsLauncher(input);
|
|
9
9
|
const reader = createInterface({ input: child.stdout });
|
|
10
10
|
let startedResolve;
|
|
@@ -23,7 +23,6 @@ function createWindowsContext(input, log, from, closeLog) {
|
|
|
23
23
|
child.stderr?.setEncoding("utf8");
|
|
24
24
|
const context = {
|
|
25
25
|
input,
|
|
26
|
-
log,
|
|
27
26
|
from,
|
|
28
27
|
closeLog,
|
|
29
28
|
child,
|
|
@@ -31,6 +30,7 @@ function createWindowsContext(input, log, from, closeLog) {
|
|
|
31
30
|
stderr: "",
|
|
32
31
|
started: false,
|
|
33
32
|
targetPid: undefined,
|
|
33
|
+
released: false,
|
|
34
34
|
finished: false,
|
|
35
35
|
startedPromise,
|
|
36
36
|
startedResolve,
|
|
@@ -45,6 +45,8 @@ function createWindowsContext(input, log, from, closeLog) {
|
|
|
45
45
|
return context;
|
|
46
46
|
}
|
|
47
47
|
function failWindows(context, error) {
|
|
48
|
+
if (context.released)
|
|
49
|
+
return;
|
|
48
50
|
context.lifecycle?.markInert();
|
|
49
51
|
if (!context.started)
|
|
50
52
|
context.startedReject(error);
|
|
@@ -76,14 +78,16 @@ function recordWindowsExit(context, code) {
|
|
|
76
78
|
void (async () => {
|
|
77
79
|
let failure;
|
|
78
80
|
let result;
|
|
81
|
+
let exitLog;
|
|
79
82
|
try {
|
|
80
|
-
|
|
83
|
+
exitLog = await open(context.input.log, "r+");
|
|
84
|
+
result = await retainDetachedExitEvidence(exitLog, context.input.log, context.from, code, null);
|
|
81
85
|
}
|
|
82
86
|
catch (error) {
|
|
83
87
|
failure = error;
|
|
84
88
|
}
|
|
85
89
|
try {
|
|
86
|
-
await
|
|
90
|
+
await exitLog?.close();
|
|
87
91
|
}
|
|
88
92
|
catch (error) {
|
|
89
93
|
failure ??= error;
|
|
@@ -95,6 +99,7 @@ function recordWindowsExit(context, code) {
|
|
|
95
99
|
const status = detachedExitStatus(code, null);
|
|
96
100
|
throw new Error(`pre-admission ${status}: run-log evidence unavailable: ${failure instanceof Error ? failure.message : String(failure)}`);
|
|
97
101
|
}
|
|
102
|
+
await settleWindowsTermination(Promise.resolve());
|
|
98
103
|
context.exitResolve(result);
|
|
99
104
|
})().catch(context.exitReject);
|
|
100
105
|
}
|
|
@@ -126,6 +131,8 @@ function handleWindowsLine(context, line) {
|
|
|
126
131
|
function installWindowsProtocol(context) {
|
|
127
132
|
context.child.once("error", (error) => failWindows(context, error));
|
|
128
133
|
context.child.once("close", () => {
|
|
134
|
+
if (context.released)
|
|
135
|
+
return;
|
|
129
136
|
if (!context.started) {
|
|
130
137
|
failWindows(context, launchFailure(context.input.argv[0] ?? "windows-launch.exe", context.stderr.trim()));
|
|
131
138
|
}
|
|
@@ -138,8 +145,9 @@ function installWindowsProtocol(context) {
|
|
|
138
145
|
function ownedWindowsProcess(context, pid) {
|
|
139
146
|
const lifecycle = createProcessLifecycle(async () => {
|
|
140
147
|
await terminateWindowsTree(pid);
|
|
141
|
-
await context.exited;
|
|
148
|
+
await settleWindowsTermination(context.exited.then(() => undefined));
|
|
142
149
|
}, () => {
|
|
150
|
+
context.released = true;
|
|
143
151
|
context.reader.close();
|
|
144
152
|
context.child.stdin?.write("release\n");
|
|
145
153
|
context.child.stdin?.end();
|
|
@@ -164,10 +172,11 @@ export async function spawnWindowsRetainedProcess(input) {
|
|
|
164
172
|
logClosed = true;
|
|
165
173
|
await log.close();
|
|
166
174
|
};
|
|
167
|
-
const context = createWindowsContext(input,
|
|
175
|
+
const context = createWindowsContext(input, (await log.stat()).size, closeLog);
|
|
168
176
|
installWindowsProtocol(context);
|
|
169
177
|
try {
|
|
170
178
|
const pid = await context.startedPromise;
|
|
179
|
+
await closeLog();
|
|
171
180
|
return ownedWindowsProcess(context, pid);
|
|
172
181
|
}
|
|
173
182
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/keiyaku",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"build"
|
|
6
6
|
],
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
61
61
|
"@anthropic-ai/claude-agent-sdk": "^0.3.226",
|
|
62
|
-
"@astrosheep/square": "0.3.
|
|
62
|
+
"@astrosheep/square": "0.3.17",
|
|
63
63
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
64
64
|
"@opencode-ai/sdk": "1.18.3",
|
|
65
65
|
"cross-spawn": "^7.0.6",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|