@astrosheep/keiyaku 4.6.13 → 4.6.14
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/plugins/claude/skills/keiyaku/SKILL.md +3 -0
- package/build/plugins/claude/skills/keiyaku-akuma/SKILL.md +20 -0
- package/build/plugins/codex/skills/keiyaku/SKILL.md +3 -0
- package/build/plugins/codex/skills/keiyaku-akuma/SKILL.md +20 -0
- package/build/plugins/opencode/skills/keiyaku/SKILL.md +3 -0
- package/build/plugins/opencode/skills/keiyaku-akuma/SKILL.md +20 -0
- package/build/skills/keiyaku/SKILL.md +3 -0
- package/build/skills/keiyaku-akuma/SKILL.md +20 -0
- package/build/src/akuma/akuma-instance.js +7 -5
- package/build/src/akuma/akuma-observe.js +1 -0
- package/build/src/akuma/akuma-owner.js +3 -2
- package/build/src/akuma/akuma-wait.d.ts +1 -2
- package/build/src/akuma/akuma-wait.js +1 -4
- package/build/src/akuma/akuma.d.ts +9 -4
- package/build/src/akuma/akuma.js +208 -35
- package/build/src/akuma/body.js +4 -0
- package/build/src/akuma/caller.d.ts +2 -0
- package/build/src/akuma/caller.js +6 -0
- package/build/src/akuma/custody.d.ts +11 -0
- package/build/src/akuma/custody.js +102 -0
- package/build/src/akuma/lineage-cache.d.ts +39 -0
- package/build/src/akuma/lineage-cache.js +162 -0
- package/build/src/akuma/nuke.d.ts +1 -3
- package/build/src/akuma/nuke.js +11 -102
- package/build/src/akuma/retention.d.ts +44 -0
- package/build/src/akuma/retention.js +106 -0
- package/build/src/akuma/selection-owner-port.d.ts +1 -1
- package/build/src/akuma/selection-owner-port.js +3 -6
- package/build/src/akuma/selection-request.d.ts +131 -4
- package/build/src/akuma/selection-request.js +25 -15
- package/build/src/alias/index.d.ts +11 -0
- package/build/src/alias/index.js +20 -1
- package/build/src/cli/commands/contract-grammar.d.ts +12 -0
- package/build/src/cli/commands/contract-grammar.js +65 -13
- package/build/src/cli/commands/contract-help.d.ts +3 -1
- package/build/src/cli/commands/contract-help.js +5 -2
- package/build/src/cli/commands/contract.js +31 -4
- package/build/src/cli/render/catalog.d.ts +2 -1
- package/build/src/cli/render/catalog.js +8 -2
- package/build/src/cli/square-identity.d.ts +0 -5
- package/build/src/cli/square-identity.js +51 -101
- package/build/src/index.d.ts +1 -1
- package/build/src/library/akuma-retention.d.ts +11 -0
- package/build/src/library/akuma-retention.js +131 -0
- package/build/src/library/akumas.d.ts +8 -0
- package/build/src/library/akumas.js +42 -2
- package/build/src/library/selection.js +22 -9
- package/build/src/plugin/akuma-signals.d.ts +1 -0
- package/build/src/plugin/akuma-signals.js +17 -0
- package/build/src/plugin/public.d.ts +7 -0
- package/build/src/plugin/runtime.d.ts +1 -0
- package/build/src/plugin/runtime.js +85 -13
- package/build/src/runtime/proc/windows-launch.exe +0 -0
- package/package.json +3 -3
- package/skills/keiyaku/SKILL.md +3 -0
- package/skills/keiyaku-akuma/SKILL.md +20 -0
|
@@ -1,61 +1,60 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { inbox, Square, SquareError, squareAssignedParticipantName } from "@astrosheep/square";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Square, SquareError } from "@astrosheep/square";
|
|
5
4
|
/**
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
* know how Square names a participant. Every Square read below is rooted in the
|
|
9
|
-
* submitted environment's own coordinates, never in this process's ambient ones.
|
|
5
|
+
* The submitting-edge identity policy. Every Square read is rooted in the
|
|
6
|
+
* submitted environment's actual Body, never this process's ambient identity.
|
|
10
7
|
*/
|
|
11
|
-
/** Square's own harness-session coordinates, in its resolution order. */
|
|
12
8
|
const HARNESS_SESSIONS = [
|
|
13
|
-
{ provider: "claude", variable: "CLAUDE_CODE_SESSION_ID" },
|
|
14
|
-
{ provider: "codex", variable: "CODEX_THREAD_ID" },
|
|
15
|
-
{ provider: "opencode", variable: "OPENCODE_SESSION_ID" },
|
|
16
|
-
{ provider: "pi", variable: "PI_SESSION_ID" },
|
|
17
|
-
{ provider: undefined, variable: "PASEO_AGENT_ID" },
|
|
9
|
+
{ provider: "claude", channel: "claude-code", variable: "CLAUDE_CODE_SESSION_ID" },
|
|
10
|
+
{ provider: "codex", channel: "codex", variable: "CODEX_THREAD_ID" },
|
|
11
|
+
{ provider: "opencode", channel: "opencode", variable: "OPENCODE_SESSION_ID" },
|
|
12
|
+
{ provider: "pi", channel: "pi", variable: "PI_SESSION_ID" },
|
|
13
|
+
{ provider: undefined, channel: "paseo", variable: "PASEO_AGENT_ID" },
|
|
18
14
|
];
|
|
19
15
|
const SEAT_PREFIX = "keiyaku-assigned-";
|
|
20
16
|
const SEAT_DIGEST_DOMAIN = "keiyaku/square-seat/v1";
|
|
21
17
|
const SEAT_DIGEST_LENGTH = 12;
|
|
22
|
-
/** One bounded identity observation; a non-cooperative Square never holds a submission. */
|
|
23
18
|
const IDENTITY_BUDGET_MS = 5_000;
|
|
24
19
|
function declared(environment, variable) {
|
|
25
20
|
const value = environment[variable]?.trim();
|
|
26
21
|
return value === undefined || value === "" ? undefined : value;
|
|
27
22
|
}
|
|
28
|
-
|
|
23
|
+
function sessionCoordinates(environment) {
|
|
24
|
+
return HARNESS_SESSIONS.flatMap(({ variable, channel, provider }) => {
|
|
25
|
+
const session = declared(environment, variable);
|
|
26
|
+
return session === undefined ? [] : [{ channel, ...(provider === undefined ? {} : { provider }), session }];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function squareBody(environment) {
|
|
30
|
+
if (environment.CLAUDE_CODE_CHILD_SESSION === "1")
|
|
31
|
+
return undefined;
|
|
32
|
+
const coordinates = sessionCoordinates(environment);
|
|
33
|
+
const native = coordinates.filter(({ provider }) => provider !== undefined);
|
|
34
|
+
if (native.length > 1)
|
|
35
|
+
return undefined;
|
|
36
|
+
const coordinate = native[0] ?? coordinates.find(({ channel }) => channel === "paseo");
|
|
37
|
+
return coordinate === undefined ? undefined : { channel: coordinate.channel, session: coordinate.session };
|
|
38
|
+
}
|
|
29
39
|
function squareSeatName(provider, session) {
|
|
30
40
|
const digest = createHash("sha256")
|
|
31
41
|
.update(`${SEAT_DIGEST_DOMAIN}\u0000${provider}\u0000${session}`, "utf8")
|
|
32
42
|
.digest("hex");
|
|
33
43
|
return `${SEAT_PREFIX}${digest.slice(0, SEAT_DIGEST_LENGTH)}`;
|
|
34
44
|
}
|
|
35
|
-
/** Every harness session coordinate this process declares, able to name a seat or not. */
|
|
36
|
-
function sessionIds(environment) {
|
|
37
|
-
return HARNESS_SESSIONS.flatMap(({ variable }) => {
|
|
38
|
-
const session = declared(environment, variable);
|
|
39
|
-
return session === undefined ? [] : [session];
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* The deterministic candidate for this process. Only one provider-backed session
|
|
44
|
-
* may exist: a second coordinate — another provider, or the same raw token under
|
|
45
|
-
* a different one — is ambiguous and selects nothing.
|
|
46
|
-
*/
|
|
47
45
|
function seatCandidate(environment) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const candidates = sessionCoordinates(environment).flatMap(({ provider, session }) => provider === undefined ? [] : [squareSeatName(provider, session)]);
|
|
47
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
48
|
+
}
|
|
49
|
+
function priorSeatNames(environment) {
|
|
50
|
+
return sessionCoordinates(environment).flatMap(({ provider, session }) => {
|
|
51
|
+
if (provider === undefined)
|
|
52
|
+
return [];
|
|
53
|
+
const legacy = createHash("sha256").update(session, "utf8").digest("hex").slice(0, 12);
|
|
54
|
+
const assigned = squareSeatName(provider, session);
|
|
55
|
+
return [`${provider}-${legacy}`, assigned, assigned.replace(SEAT_PREFIX, "keiyaku-seat-")];
|
|
51
56
|
});
|
|
52
|
-
return names.length === 1 ? names[0] : undefined;
|
|
53
57
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Square owns the name grammar, so the edge borrows one of Square's own handles
|
|
56
|
-
* instead of restating it. An in-memory square validates without touching any
|
|
57
|
-
* artifact.
|
|
58
|
-
*/
|
|
59
58
|
async function usableName(name) {
|
|
60
59
|
try {
|
|
61
60
|
await Square.inMemory({ markdown: "" }).participant(name);
|
|
@@ -65,66 +64,27 @@ async function usableName(name) {
|
|
|
65
64
|
return false;
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
|
-
|
|
69
|
-
async function canonical(path) {
|
|
70
|
-
try {
|
|
71
|
-
return await realpath(path);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return resolve(path);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Bindings this session already holds in this Square. Square's session inbox
|
|
79
|
-
* reads the same host registry as native hooks, and it is the only public read
|
|
80
|
-
* that takes the caller's own environment coordinate. Its page is bounded, so a
|
|
81
|
-
* view that hides rows proves nothing about absence here.
|
|
82
|
-
*/
|
|
83
|
-
async function joinedBindings(environment, ids, location, signal) {
|
|
84
|
-
const pages = await Promise.all(ids.map(async (session) => await inbox({ env: environment }, session, {}, { signal })));
|
|
85
|
-
const here = await canonical(location);
|
|
86
|
-
return {
|
|
87
|
-
here: pages.flatMap(({ rows }) => rows).filter(({ squarePath }) => squarePath === here).length,
|
|
88
|
-
complete: pages.every(({ rows, total }) => total === rows.length),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* A session whose former participant already finished here has no replacement
|
|
93
|
-
* identity: releasing the binding leaves Square's roster as the only evidence of
|
|
94
|
-
* that standing, and a fresh seat would rename a finished caller into a new one.
|
|
95
|
-
*/
|
|
96
|
-
async function finishedHere(square, environment, signal) {
|
|
97
|
-
const former = squareAssignedParticipantName(environment);
|
|
98
|
-
const previous = seatCandidate(environment)?.replace(SEAT_PREFIX, "keiyaku-seat-");
|
|
99
|
-
if (former === undefined && previous === undefined)
|
|
100
|
-
return false;
|
|
101
|
-
const { participants } = await square.participants({}, { signal });
|
|
102
|
-
return participants.some(({ name, state }) => (name === former || name === previous) && state === "done");
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* A binding this edge cannot stand on is never replaced by a fresh name: Square
|
|
106
|
-
* adjudicates standing, ownership, and lifecycle. The edge only distinguishes a
|
|
107
|
-
* Square that is genuinely not there yet from evidence it cannot trust.
|
|
108
|
-
*/
|
|
109
|
-
async function standingAt(location, environment, ids, signal) {
|
|
67
|
+
async function standingAt(location, environment, body, signal) {
|
|
110
68
|
let square;
|
|
111
69
|
try {
|
|
112
|
-
square = await Square.at({ path: location, env: environment }, { signal });
|
|
70
|
+
square = await Square.at({ path: location, env: environment, body }, { signal });
|
|
113
71
|
}
|
|
114
72
|
catch (error) {
|
|
115
|
-
// Only a Square that is genuinely not there yet may receive a new seat.
|
|
116
73
|
return error instanceof SquareError && error.code === "unavailable"
|
|
117
74
|
? { kind: "absent" }
|
|
118
75
|
: { kind: "indeterminate" };
|
|
119
76
|
}
|
|
120
77
|
try {
|
|
121
|
-
const recognized = await square.recognize(
|
|
122
|
-
if (recognized
|
|
123
|
-
return { kind: "standing", name: recognized.name };
|
|
124
|
-
|
|
125
|
-
|
|
78
|
+
const recognized = await square.recognize();
|
|
79
|
+
if (recognized.length === 1)
|
|
80
|
+
return { kind: "standing", name: recognized[0].name };
|
|
81
|
+
if (recognized.length > 1)
|
|
82
|
+
return { kind: "indeterminate" };
|
|
83
|
+
const known = new Set(priorSeatNames(environment));
|
|
84
|
+
const participants = (await square.participants({}, { signal })).participants;
|
|
85
|
+
if (participants.some(({ name, state }) => known.has(name) && state !== "not joined"))
|
|
126
86
|
return { kind: "indeterminate" };
|
|
127
|
-
return
|
|
87
|
+
return { kind: "absent" };
|
|
128
88
|
}
|
|
129
89
|
catch {
|
|
130
90
|
return { kind: "indeterminate" };
|
|
@@ -133,11 +93,6 @@ async function standingAt(location, environment, ids, signal) {
|
|
|
133
93
|
await square.close().catch(() => undefined);
|
|
134
94
|
}
|
|
135
95
|
}
|
|
136
|
-
/**
|
|
137
|
-
* The one bounded attempt. A late settlement keeps no authority: the adapter only
|
|
138
|
-
* reads Square, so nothing arrives after the caller has settled, and no refusal
|
|
139
|
-
* escapes as an unhandled rejection.
|
|
140
|
-
*/
|
|
141
96
|
async function bounded(attempt, control) {
|
|
142
97
|
const budget = new AbortController();
|
|
143
98
|
const timer = setTimeout(() => budget.abort(new Error("Square identity observation expired")), control.budgetMs ?? IDENTITY_BUDGET_MS);
|
|
@@ -159,22 +114,17 @@ function whenAborted(signal) {
|
|
|
159
114
|
signal.addEventListener("abort", () => settle(undefined), { once: true });
|
|
160
115
|
});
|
|
161
116
|
}
|
|
162
|
-
/**
|
|
163
|
-
* The Square participant name this submitting session should speak as, or
|
|
164
|
-
* undefined when no honest choice exists. Identity preparation is optional, so it
|
|
165
|
-
* never refuses the enclosing operation and always settles within one budget.
|
|
166
|
-
*/
|
|
167
117
|
export async function squareSeat(input, control = {}) {
|
|
168
118
|
const name = declared(input.environment, "SQUARE_PARTICIPANT_NAME");
|
|
169
119
|
if (name !== undefined)
|
|
170
120
|
return (await usableName(name)) ? name : undefined;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
if (
|
|
121
|
+
const body = squareBody(input.environment);
|
|
122
|
+
const candidate = seatCandidate(input.environment);
|
|
123
|
+
if (body === undefined)
|
|
174
124
|
return undefined;
|
|
175
125
|
const location = join(input.path, ".square", "KEIYAKU.square");
|
|
176
|
-
const standing = await bounded(async (signal) => await standingAt(location, input.environment,
|
|
126
|
+
const standing = await bounded(async (signal) => await standingAt(location, input.environment, body, signal), control);
|
|
177
127
|
if (standing?.kind === "standing")
|
|
178
128
|
return standing.name;
|
|
179
|
-
return standing?.kind === "absent" ?
|
|
129
|
+
return standing?.kind === "absent" ? candidate : undefined;
|
|
180
130
|
}
|
package/build/src/index.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export type { ReconcileCompletion, RepoContractReconcileReport, RepoReconcileRep
|
|
|
63
63
|
export type { KeiyakuRefusal, KeiyakuRetryReason, NukeConfirmationRefusal, NukeConfirmationRequiredRefusal, OperationRetries, } from "./library/refusal.js";
|
|
64
64
|
export type { RegionOverlap } from "./library/region.js";
|
|
65
65
|
export type { RepoAtInput } from "./library/repo.js";
|
|
66
|
-
export type { KeiyakuPlugin, PluginAkumaCaller, PluginAkumaOrigin, PluginBodyEnd, PluginContext, PluginHooks, PluginInstance, PluginManifest, PluginOutcome, PluginSignal, PluginSignalMap, } from "./plugin/public.js";
|
|
66
|
+
export type { KeiyakuPlugin, PluginAkumaCaller, PluginAkumaOrigin, PluginBodyEnd, PluginContext, PluginHooks, PluginInstance, PluginManifest, PluginOutcome, PluginWaitDeferral, PluginWaitDeferralInput, PluginSignal, PluginSignalMap, } from "./plugin/public.js";
|
|
67
67
|
export type { DeliveryPreviewReport } from "./protocol/delivery-preview.js";
|
|
68
68
|
export type { IntegrationConflictMaterialized, VerificationReuse } from "./protocol/deliver.js";
|
|
69
69
|
export type { ExecutionObservation, ExecutionObserver } from "./protocol/execution-observation.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AkuId } from "../akuma/identity.js";
|
|
2
|
+
import type { WorldRoot } from "../world.js";
|
|
3
|
+
/**
|
|
4
|
+
* One deterministic, bounded retention sweep over the World's recognized Akuma
|
|
5
|
+
* entries. The semantic age, lifecycle, and custody judgment stay in the Akuma
|
|
6
|
+
* owner; this composition supplies only the optional Alias, Dispatch, and
|
|
7
|
+
* Contract associations that owner must not read itself.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sweepInactiveAkumas(world: WorldRoot, options?: Readonly<{
|
|
10
|
+
now?: number;
|
|
11
|
+
}>): Promise<readonly AkuId[]>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** @architectureCompositionRoot */
|
|
2
|
+
import { tryAcquireAliasCustody, readAliases } from "../alias/index.js";
|
|
3
|
+
import { knownAkuma } from "../akuma/akuma.js";
|
|
4
|
+
import { boundedMap } from "../akuma/read-pool.js";
|
|
5
|
+
import { readRetentionCandidate, retentionCandidateBound, retentionNow, retireInactiveAkuma, } from "../akuma/retention.js";
|
|
6
|
+
import { observeDispatchAssociations } from "../dispatch/index.js";
|
|
7
|
+
import { observeContractAt } from "../git/observe.js";
|
|
8
|
+
import { isPrivateStateSeatContention, withPrivateStatePublicationSeat } from "../git/private-state-seat.js";
|
|
9
|
+
import { withGitDecodeChannel } from "../git/read-observation.js";
|
|
10
|
+
import { NoGitWorldError, repositoryAt } from "../git/repository.js";
|
|
11
|
+
/**
|
|
12
|
+
* A World that never became a Git repository has no Dispatch or Contract
|
|
13
|
+
* authority, so every candidate is free of those associations. Any other
|
|
14
|
+
* failure to prove the repository keeps all candidates.
|
|
15
|
+
*/
|
|
16
|
+
async function retentionRepository(world) {
|
|
17
|
+
try {
|
|
18
|
+
return await repositoryAt(world);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof NoGitWorldError)
|
|
22
|
+
return undefined;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A Dispatch association is retirable only when it is absent or names a
|
|
28
|
+
* terminal Contract. Unproven, corrupt, missing-Contract, and active Contract
|
|
29
|
+
* associations all keep the Akuma.
|
|
30
|
+
*/
|
|
31
|
+
async function dispatchAssociationIsRetirable(repository, association) {
|
|
32
|
+
if (association.kind === "none")
|
|
33
|
+
return true;
|
|
34
|
+
if (association.kind === "failed" || repository === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
return await withGitDecodeChannel(repository, async (channel) => {
|
|
37
|
+
try {
|
|
38
|
+
const terminal = (await observeContractAt(repository, channel, association.contractId)).state?.terminal;
|
|
39
|
+
return terminal?.kind === "claimed" || terminal?.kind === "abandoned";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function retireCandidate(world, repository, entry, now) {
|
|
47
|
+
try {
|
|
48
|
+
const receipt = await retireInactiveAkuma({
|
|
49
|
+
entry,
|
|
50
|
+
now,
|
|
51
|
+
retireUnderCustody: async (retire) => {
|
|
52
|
+
const retireOrRetain = async () => ((await retire()) ? "retired" : "retained");
|
|
53
|
+
// Alias writers use this same authority lock. Holding it across the
|
|
54
|
+
// Dispatch judgment and removal means an Alias cannot appear between
|
|
55
|
+
// the final check and the deletion.
|
|
56
|
+
const aliases = await tryAcquireAliasCustody(world);
|
|
57
|
+
if (aliases === null)
|
|
58
|
+
return "retained";
|
|
59
|
+
try {
|
|
60
|
+
if (aliases.bindings.some((binding) => binding.akuId === entry.id))
|
|
61
|
+
return "retained";
|
|
62
|
+
if (repository === undefined)
|
|
63
|
+
return await retireOrRetain();
|
|
64
|
+
try {
|
|
65
|
+
// Dispatch publication takes this same private-state seat. Holding
|
|
66
|
+
// it across the judgment and removal means a Dispatch cannot appear
|
|
67
|
+
// between the final check and the deletion.
|
|
68
|
+
const retired = await withPrivateStatePublicationSeat(repository, async () => {
|
|
69
|
+
const associations = await observeDispatchAssociations(repository, [entry.id]);
|
|
70
|
+
if (!(await dispatchAssociationIsRetirable(repository, associations.get(entry.id))))
|
|
71
|
+
return "retained";
|
|
72
|
+
return await retireOrRetain();
|
|
73
|
+
}, { timeoutMs: 0 });
|
|
74
|
+
return retired.value;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (isPrivateStateSeatContention(error))
|
|
78
|
+
return "retained";
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
aliases.release();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return receipt === "retired";
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// One unreadable, corrupt, or unremovable candidate never fails the sweep
|
|
91
|
+
// or the ordinary operation that triggered it.
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One deterministic, bounded retention sweep over the World's recognized Akuma
|
|
97
|
+
* entries. The semantic age, lifecycle, and custody judgment stay in the Akuma
|
|
98
|
+
* owner; this composition supplies only the optional Alias, Dispatch, and
|
|
99
|
+
* Contract associations that owner must not read itself.
|
|
100
|
+
*/
|
|
101
|
+
export async function sweepInactiveAkumas(world, options = {}) {
|
|
102
|
+
const now = options.now ?? retentionNow();
|
|
103
|
+
const known = await knownAkuma(world);
|
|
104
|
+
if (known.rows.length === 0)
|
|
105
|
+
return [];
|
|
106
|
+
const candidates = (await boundedMap(known.rows, async (entry) => {
|
|
107
|
+
try {
|
|
108
|
+
if (!(await retentionCandidateBound(entry.paths, now)))
|
|
109
|
+
return null;
|
|
110
|
+
return await readRetentionCandidate(entry, now);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
})).filter((entry) => entry !== null);
|
|
116
|
+
if (candidates.length === 0)
|
|
117
|
+
return [];
|
|
118
|
+
const aliases = new Set((await readAliases(world)).map((binding) => binding.akuId));
|
|
119
|
+
const repository = await retentionRepository(world);
|
|
120
|
+
const associations = await observeDispatchAssociations(repository, candidates.map((entry) => entry.id));
|
|
121
|
+
const retired = [];
|
|
122
|
+
for (const entry of candidates) {
|
|
123
|
+
if (aliases.has(entry.id))
|
|
124
|
+
continue;
|
|
125
|
+
if (!(await dispatchAssociationIsRetirable(repository, associations.get(entry.id))))
|
|
126
|
+
continue;
|
|
127
|
+
if (await retireCandidate(world, repository, entry, now))
|
|
128
|
+
retired.push(entry.id);
|
|
129
|
+
}
|
|
130
|
+
return retired;
|
|
131
|
+
}
|
|
@@ -28,6 +28,14 @@ declare class AkumasHandle {
|
|
|
28
28
|
wait(input: AkumaWaitInput): Promise<AkumaWaitResult>;
|
|
29
29
|
kill(input: AkumaKillInput): Promise<AkumaKillResult>;
|
|
30
30
|
history(input: AkumaHistoryInput): Promise<AkumaHistoryResult>;
|
|
31
|
+
/**
|
|
32
|
+
* The one write-side maintenance point: a successful birth is already durable,
|
|
33
|
+
* so one best-effort sweep may follow it. Only an in-flight sweep is
|
|
34
|
+
* coalesced; a completed or failed sweep leaves the next successful birth free
|
|
35
|
+
* to start a fresh one. It never changes the birth result or any observation.
|
|
36
|
+
*/
|
|
37
|
+
private maintain;
|
|
38
|
+
private sweep;
|
|
31
39
|
private withWorld;
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
@@ -4,6 +4,7 @@ import { readAkumaRoster } from "../akuma/akuma.js";
|
|
|
4
4
|
import { localExecutionContext } from "../akuma/requests.js";
|
|
5
5
|
import { readAliases } from "../alias/index.js";
|
|
6
6
|
import { World } from "../world.js";
|
|
7
|
+
import { sweepInactiveAkumas } from "./akuma-retention.js";
|
|
7
8
|
import { callAkumas, forkAkumas } from "./akuma-creation.js";
|
|
8
9
|
import { requireInput } from "./input.js";
|
|
9
10
|
import { askAkumaOn, historyAkuma, killAkumaOn, selectionSeam, statusAkuma, tellAkumaOn, waitAkumaOn, } from "./selection.js";
|
|
@@ -13,6 +14,7 @@ class AkumasHandle {
|
|
|
13
14
|
#execution;
|
|
14
15
|
#selection;
|
|
15
16
|
#reportDiagnostic;
|
|
17
|
+
#maintenance;
|
|
16
18
|
constructor(world, execution, host) {
|
|
17
19
|
this.#world = world;
|
|
18
20
|
this.#execution = execution;
|
|
@@ -29,10 +31,19 @@ class AkumasHandle {
|
|
|
29
31
|
return withRosterAliases(roster, await readAliases(path));
|
|
30
32
|
}
|
|
31
33
|
call(input) {
|
|
32
|
-
|
|
34
|
+
const pending = callAkumas(this.withWorld(input, "Akumas.call input"), this.#execution, this.#reportDiagnostic);
|
|
35
|
+
return pending.then(async (result) => {
|
|
36
|
+
await this.maintain(this.#world);
|
|
37
|
+
return result;
|
|
38
|
+
});
|
|
33
39
|
}
|
|
34
40
|
fork(input) {
|
|
35
|
-
|
|
41
|
+
const pending = forkAkumas(this.withWorld(input, "Akumas.fork input"));
|
|
42
|
+
return pending.then(async (result) => {
|
|
43
|
+
if (result.kind === "forked")
|
|
44
|
+
await this.maintain(this.#world);
|
|
45
|
+
return result;
|
|
46
|
+
});
|
|
36
47
|
}
|
|
37
48
|
status(input) {
|
|
38
49
|
return statusAkuma(this.withWorld(input, "Akumas.status input"), this.#reportDiagnostic);
|
|
@@ -52,6 +63,35 @@ class AkumasHandle {
|
|
|
52
63
|
history(input) {
|
|
53
64
|
return historyAkuma(this.withWorld(input, "Akumas.history input"), this.#reportDiagnostic);
|
|
54
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The one write-side maintenance point: a successful birth is already durable,
|
|
68
|
+
* so one best-effort sweep may follow it. Only an in-flight sweep is
|
|
69
|
+
* coalesced; a completed or failed sweep leaves the next successful birth free
|
|
70
|
+
* to start a fresh one. It never changes the birth result or any observation.
|
|
71
|
+
*/
|
|
72
|
+
maintain(world) {
|
|
73
|
+
const inFlight = this.#maintenance;
|
|
74
|
+
if (inFlight !== undefined)
|
|
75
|
+
return inFlight;
|
|
76
|
+
const pending = this.sweep(world).finally(() => {
|
|
77
|
+
this.#maintenance = undefined;
|
|
78
|
+
});
|
|
79
|
+
this.#maintenance = pending;
|
|
80
|
+
return pending;
|
|
81
|
+
}
|
|
82
|
+
async sweep(world) {
|
|
83
|
+
try {
|
|
84
|
+
await sweepInactiveAkumas(world);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
try {
|
|
88
|
+
this.#reportDiagnostic?.(`akuma retention sweep failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// A diagnostic sink failure cannot change the ordinary operation.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
55
95
|
withWorld(input, label) {
|
|
56
96
|
const values = requireInput(input, label);
|
|
57
97
|
if (Object.hasOwn(values, "path"))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @architectureCompositionRoot */
|
|
2
|
+
import { observeStatusOutcome } from "../akuma/akuma-observe.js";
|
|
2
3
|
import { withoutReportedChanges } from "../akuma/projection.js";
|
|
3
4
|
import { AkumaNotBornError, AkumaObservationError } from "../akuma/akuma-errors.js";
|
|
4
5
|
import { AkumaOwner } from "../akuma/akuma-owner.js";
|
|
@@ -12,15 +13,26 @@ import { observeDispatchAssociations } from "../dispatch/index.js";
|
|
|
12
13
|
import { observeContractAt } from "../git/observe.js";
|
|
13
14
|
import { withGitDecodeChannel } from "../git/read-observation.js";
|
|
14
15
|
import { observeCreatedDebtObservations } from "../debt/created-observation.js";
|
|
15
|
-
import { outcomePluginObserver, stderrPluginDiagnostic } from "../plugin/akuma-signals.js";
|
|
16
|
+
import { outcomePluginObserver, stderrPluginDiagnostic, withAkumaWaitDeferrals } from "../plugin/akuma-signals.js";
|
|
16
17
|
import { addressAkuma, addressAkumaSet, resolveAkuma, resolveAkumaSet, } from "./address.js";
|
|
17
18
|
import { requireInput } from "./input.js";
|
|
18
19
|
import { scopeForRepo } from "./repo.js";
|
|
19
20
|
export function selectionSeam(execution, reportDiagnostic = stderrPluginDiagnostic) {
|
|
20
21
|
const channel = executionChannel(execution);
|
|
21
|
-
|
|
22
|
+
const routed = channel.kind === "body-request"
|
|
23
|
+
? forwardedSelection(channel.directory, reportDiagnostic)
|
|
24
|
+
: localSelection(reportDiagnostic);
|
|
25
|
+
return {
|
|
26
|
+
...routed,
|
|
27
|
+
wait: async (addressed, mode) => await withAkumaWaitDeferrals({ world: addressed.path, reportDiagnostic }, addressed.orderedIds, mode.signal, async () => {
|
|
28
|
+
const result = await routed.wait(addressed, mode);
|
|
29
|
+
const observer = outcomePluginObserver({ world: addressed.path, reportDiagnostic });
|
|
30
|
+
await Promise.all(result.observations.map(({ status }) => observeStatusOutcome(status, observer)));
|
|
31
|
+
return result;
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
22
34
|
}
|
|
23
|
-
function forwardedSelection(directory) {
|
|
35
|
+
function forwardedSelection(directory, reportDiagnostic) {
|
|
24
36
|
return {
|
|
25
37
|
addressOne: resolveAkuma,
|
|
26
38
|
addressSet: resolveAkumaSet,
|
|
@@ -39,7 +51,7 @@ function forwardedSelection(directory) {
|
|
|
39
51
|
...(abort === undefined ? {} : { signal: abort }),
|
|
40
52
|
}),
|
|
41
53
|
ask: async (addressed, input, abort) => {
|
|
42
|
-
const result = await requestForwardedSelectionAsk({
|
|
54
|
+
const { result, historyIds } = await requestForwardedSelectionAsk({
|
|
43
55
|
directory,
|
|
44
56
|
target: addressed.id,
|
|
45
57
|
body: input.body,
|
|
@@ -49,6 +61,7 @@ function forwardedSelection(directory) {
|
|
|
49
61
|
...(input.initiator === undefined ? {} : { initiator: input.initiator }),
|
|
50
62
|
...(abort === undefined ? {} : { signal: abort }),
|
|
51
63
|
});
|
|
64
|
+
await outcomePluginObserver({ world: addressed.path, reportDiagnostic })(result.akuma, historyIds);
|
|
52
65
|
return (input.schema === undefined
|
|
53
66
|
? result
|
|
54
67
|
: { ...result, observation: decodeAskObservation(result.observation, input.schema) });
|
|
@@ -59,7 +72,7 @@ function localSelection(reportDiagnostic) {
|
|
|
59
72
|
return {
|
|
60
73
|
addressOne: addressAkuma,
|
|
61
74
|
addressSet: addressAkumaSet,
|
|
62
|
-
wait: async (addressed, mode) => await localWait(addressed, mode
|
|
75
|
+
wait: async (addressed, mode) => await localWait(addressed, mode),
|
|
63
76
|
kill: async (addressed, abort) => await executeKillAkuma({
|
|
64
77
|
path: addressed.path,
|
|
65
78
|
ids: addressed.orderedIds,
|
|
@@ -231,21 +244,21 @@ function completionMode(value) {
|
|
|
231
244
|
return value;
|
|
232
245
|
}
|
|
233
246
|
async function forwardedWait(addressed, input) {
|
|
234
|
-
|
|
247
|
+
const result = await requestForwardedSelectionWait({
|
|
235
248
|
directory: input.directory,
|
|
236
249
|
targets: addressed.orderedIds,
|
|
237
250
|
completion: input.completion,
|
|
238
251
|
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
239
252
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
240
|
-
})
|
|
253
|
+
});
|
|
254
|
+
return await attachWaitAssociations(addressed.path, input.repo, result);
|
|
241
255
|
}
|
|
242
|
-
async function localWait(addressed, input
|
|
256
|
+
async function localWait(addressed, input) {
|
|
243
257
|
try {
|
|
244
258
|
return await composeWaitResult(addressed.path, input.repo, await executeWaitAkuma({
|
|
245
259
|
path: addressed.path,
|
|
246
260
|
ids: addressed.orderedIds,
|
|
247
261
|
completion: input.completion,
|
|
248
|
-
outcomeObserved: outcomePluginObserver({ world: addressed.path, reportDiagnostic }),
|
|
249
262
|
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
250
263
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
251
264
|
}));
|
|
@@ -14,6 +14,7 @@ type PluginSignalEmission = Readonly<{
|
|
|
14
14
|
* problem reaches the producer only as a diagnostic, never as a thrown error.
|
|
15
15
|
*/
|
|
16
16
|
export declare function emitPluginSignal(emission: PluginSignalEmission, signal: PluginSignal): Promise<void>;
|
|
17
|
+
export declare function withAkumaWaitDeferrals<T>(emission: PluginSignalEmission, akumaIds: readonly string[], signal: AbortSignal | undefined, action: () => Promise<T>): Promise<T>;
|
|
17
18
|
/** One public observer operation, never shared with incidental owner reads. */
|
|
18
19
|
export declare function outcomePluginObserver(emission: PluginSignalEmission, observer?: string): (akumaId: string, historyIds: readonly string[]) => Promise<void>;
|
|
19
20
|
export {};
|
|
@@ -21,6 +21,23 @@ export async function emitPluginSignal(emission, signal) {
|
|
|
21
21
|
reportPluginDiagnostic(emission.reportDiagnostic, "runtime", "signal", error);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
export async function withAkumaWaitDeferrals(emission, akumaIds, signal, action) {
|
|
25
|
+
signal?.throwIfAborted();
|
|
26
|
+
let runtime;
|
|
27
|
+
try {
|
|
28
|
+
runtime = await pluginRuntime({
|
|
29
|
+
world: emission.world,
|
|
30
|
+
...(emission.settings === undefined ? {} : { settings: emission.settings }),
|
|
31
|
+
reportDiagnostic: emission.reportDiagnostic,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
reportPluginDiagnostic(emission.reportDiagnostic, "runtime", "wait deferral", error);
|
|
37
|
+
return await action();
|
|
38
|
+
}
|
|
39
|
+
return await runtime.withWaitDeferrals(akumaIds, signal, action, emission.reportDiagnostic);
|
|
40
|
+
}
|
|
24
41
|
/** One public observer operation, never shared with incidental owner reads. */
|
|
25
42
|
export function outcomePluginObserver(emission, observer) {
|
|
26
43
|
const seen = new Set();
|
|
@@ -97,6 +97,13 @@ export type KeiyakuPlugin = Readonly<{
|
|
|
97
97
|
manifest: PluginManifest;
|
|
98
98
|
activate(context: PluginContext, cancellation?: AbortSignal): Promise<PluginInstance> | PluginInstance;
|
|
99
99
|
}>;
|
|
100
|
+
export type PluginWaitDeferralInput = Readonly<{
|
|
101
|
+
akumaIds: readonly string[];
|
|
102
|
+
}>;
|
|
103
|
+
export type PluginWaitDeferral = Readonly<{
|
|
104
|
+
close(cancellation?: AbortSignal): Promise<void> | void;
|
|
105
|
+
}>;
|
|
100
106
|
export type PluginInstance = Readonly<{
|
|
101
107
|
signals?: PluginHooks;
|
|
108
|
+
waitDeferral?: (input: PluginWaitDeferralInput, cancellation?: AbortSignal) => Promise<PluginWaitDeferral | undefined> | PluginWaitDeferral | undefined;
|
|
102
109
|
}>;
|
|
@@ -9,6 +9,7 @@ type PluginRuntimeInput = Readonly<{
|
|
|
9
9
|
}>;
|
|
10
10
|
export type PluginRuntime = Readonly<{
|
|
11
11
|
emit(signal: PluginSignal, reportDiagnostic?: PluginDiagnostic): Promise<void>;
|
|
12
|
+
withWaitDeferrals<T>(akumaIds: readonly string[], signal: AbortSignal | undefined, action: () => Promise<T>, reportDiagnostic?: PluginDiagnostic): Promise<T>;
|
|
12
13
|
}>;
|
|
13
14
|
export declare function reportPluginDiagnostic(report: PluginDiagnostic | undefined, subject: string, stage: string, error: unknown): void;
|
|
14
15
|
export declare function pluginRuntime(input: PluginRuntimeInput): Promise<PluginRuntime>;
|