@astrosheep/keiyaku 1.0.1 → 1.0.2
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/README.md +5 -7
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms_v2.js +103 -0
- package/build/agents/harness/activity-values.js +54 -0
- package/build/agents/harness/event-channel.js +55 -20
- package/build/agents/harness/events.js +47 -1
- package/build/agents/harness/execution-handle.js +292 -61
- package/build/agents/harness/index.js +103 -121
- package/build/agents/harness/pump.js +36 -47
- package/build/agents/harness/runtime.js +3 -0
- package/build/agents/launch-snapshot_v2.js +348 -0
- package/build/agents/opencode-sdk.js +42 -169
- package/build/agents/providers/claude-agent-sdk.js +240 -79
- package/build/agents/providers/codex-app-server.js +280 -72
- package/build/agents/providers/codex-sdk.js +159 -41
- package/build/agents/providers/event-type-tail.js +16 -0
- package/build/agents/providers/opencode-sdk.js +165 -59
- package/build/agents/providers/pi.js +254 -42
- package/build/agents/selector_v2.js +122 -0
- package/build/cli/commands/akuma.js +120 -42
- package/build/cli/completion.js +6 -7
- package/build/cli/flags.js +36 -10
- package/build/cli/help.js +29 -24
- package/build/cli/index.js +225 -93
- package/build/cli/parse.js +194 -75
- package/build/cli/projection-address.js +11 -0
- package/build/cli/render/address.js +88 -0
- package/build/cli/render/arc.js +10 -10
- package/build/cli/render/call.js +59 -0
- package/build/cli/render/compact-text.js +3 -0
- package/build/cli/render/errors.js +4 -2
- package/build/cli/render/format.js +6 -0
- package/build/cli/render/line-width.js +70 -0
- package/build/cli/render/misc.js +7 -4
- package/build/cli/render/petition.js +4 -3
- package/build/cli/render/projection-activity.js +201 -0
- package/build/cli/render/shared.js +46 -12
- package/build/cli/render/status.js +59 -10
- package/build/cli/render/tool-ledger-rollup.js +74 -0
- package/build/cli/render/tool-presentation.js +97 -0
- package/build/cli/render/wait.js +229 -0
- package/build/cli/skills-install.js +42 -24
- package/build/cli/subagent-guard.js +1 -1
- package/build/config/akuma-loader_v2.js +222 -0
- package/build/config/env-keys.js +0 -4
- package/build/config/env.js +0 -6
- package/build/config/settings.js +325 -12
- package/build/core/addressing.js +224 -0
- package/build/core/amend.js +30 -47
- package/build/core/arc.js +68 -60
- package/build/core/bind.js +105 -25
- package/build/core/call-persist.js +69 -0
- package/build/core/call.js +658 -0
- package/build/core/claim.js +72 -44
- package/build/core/command-io.js +19 -19
- package/build/core/context.js +11 -36
- package/build/core/draft.js +2 -3
- package/build/core/entry.js +101 -6
- package/build/core/execution-coordinate.js +62 -0
- package/build/core/execution-pact.js +141 -0
- package/build/core/forfeit.js +2 -3
- package/build/core/hints.js +10 -10
- package/build/core/ids.js +6 -0
- package/build/core/ledger.js +16 -1
- package/build/core/petition-claim-gates.js +14 -12
- package/build/core/petition-claim.js +1 -0
- package/build/core/petition-run.js +6 -2
- package/build/core/petition.js +63 -21
- package/build/core/places.js +106 -13
- package/build/core/projection/generation.js +412 -0
- package/build/core/projection/heart.js +316 -0
- package/build/core/projection/identity.js +81 -0
- package/build/core/projection/leash.js +87 -0
- package/build/core/projection/mint.js +154 -0
- package/build/core/projection-activity.js +219 -0
- package/build/core/projection-coordinate.js +35 -0
- package/build/core/projection-core.js +69 -463
- package/build/core/projection-generation-continuation.js +31 -0
- package/build/core/projection-generation-execution.js +148 -0
- package/build/core/projection-generation-identity.js +17 -0
- package/build/core/projection-generation-launcher.js +90 -0
- package/build/core/projection-generation-process.js +43 -0
- package/build/core/projection-generation-runner.js +144 -0
- package/build/core/projection-generation-runtime.js +15 -0
- package/build/core/projection-generation-store.js +707 -0
- package/build/core/projection-identity.js +11 -0
- package/build/core/projection-life-observer.js +87 -0
- package/build/core/projection-life-protocol.js +95 -0
- package/build/core/projection-mint.js +96 -24
- package/build/core/projection-runner-lock.js +230 -0
- package/build/core/projection-status.js +207 -107
- package/build/core/projection-tells.js +401 -0
- package/build/core/projection-wait.js +155 -0
- package/build/core/projection-wake.js +109 -231
- package/build/core/queue_v2.js +342 -0
- package/build/core/registry.js +6 -3
- package/build/core/render.js +63 -1
- package/build/core/renew.js +80 -53
- package/build/core/scope.js +186 -137
- package/build/core/seal.js +16 -19
- package/build/core/status.js +88 -25
- package/build/core/stored-agent-event.js +101 -0
- package/build/core/transcripts.js +214 -228
- package/build/core/verdict.js +27 -15
- package/build/core/worktree-bootstrap.js +187 -0
- package/build/core/worktree-path.js +72 -18
- package/build/flow-error.js +4 -6
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +6 -1
- package/build/index.js +8 -5
- package/build/keiyaku.js +1 -3
- package/package.json +6 -5
- package/skills/keiyaku/SKILL.md +6 -47
- package/skills/keiyaku-akuma/SKILL.md +75 -0
- package/build/agents/effective-policy.js +0 -32
- package/build/agents/index.js +0 -114
- package/build/agents/selector.js +0 -28
- package/build/cli/render/summon.js +0 -17
- package/build/cli/render/tell.js +0 -11
- package/build/config/akuma-loader.js +0 -268
- package/build/core/queue.js +0 -73
- package/build/core/summon-persist.js +0 -73
- package/build/core/summon.js +0 -377
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { openProjectionGenerationStoreReadOnly, } from "./projection-generation-store.js";
|
|
2
|
+
/** Read the immutable projection identity from the heart database. */
|
|
3
|
+
export function readProjectionIdentity(projectionDirectory) {
|
|
4
|
+
const heart = openProjectionGenerationStoreReadOnly(projectionDirectory);
|
|
5
|
+
try {
|
|
6
|
+
return heart.readIdentity();
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
heart.close();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { openProjectionGenerationStoreReadOnly, } from "./projection-generation-store.js";
|
|
2
|
+
import { deriveProjectionLife, selectCurrentGeneration, } from "./projection-life-protocol.js";
|
|
3
|
+
import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
|
|
4
|
+
function errorDetail(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Observe the current generation without creating, repairing, or mutating
|
|
9
|
+
* projection storage. Generation history is the ordering authority; the
|
|
10
|
+
* runner lock is consulted only for an adopted, non-terminal generation.
|
|
11
|
+
*/
|
|
12
|
+
export function observeProjectionLifeSnapshot(projectionDirectory, options) {
|
|
13
|
+
const readRecords = () => {
|
|
14
|
+
const store = openProjectionGenerationStoreReadOnly(projectionDirectory);
|
|
15
|
+
try {
|
|
16
|
+
return store.readRecords();
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
store.close();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
23
|
+
let records;
|
|
24
|
+
try {
|
|
25
|
+
records = readRecords();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
life: {
|
|
30
|
+
phase: "unknown",
|
|
31
|
+
diagnostic: `generation history could not be observed: ${errorDetail(error)}`,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const current = selectCurrentGeneration(records);
|
|
36
|
+
if (!current || current.verdict || !current.adoption) {
|
|
37
|
+
return {
|
|
38
|
+
life: deriveProjectionLife({
|
|
39
|
+
records,
|
|
40
|
+
nowMs: options.nowMs,
|
|
41
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
42
|
+
}),
|
|
43
|
+
records,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const lock = observeProjectionRunnerLock(projectionDirectory, options.platform);
|
|
47
|
+
let after;
|
|
48
|
+
try {
|
|
49
|
+
after = readRecords();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
life: {
|
|
54
|
+
phase: "unknown",
|
|
55
|
+
executionId: current.launch.executionId,
|
|
56
|
+
diagnostic: `generation history could not be re-observed: ${errorDetail(error)}`,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const beforeTail = records.at(-1)?.seq;
|
|
61
|
+
const afterTail = after.at(-1)?.seq;
|
|
62
|
+
if (records.length !== after.length || beforeTail !== afterTail)
|
|
63
|
+
continue;
|
|
64
|
+
return {
|
|
65
|
+
life: deriveProjectionLife({
|
|
66
|
+
records,
|
|
67
|
+
runnerLock: lock.state === "held"
|
|
68
|
+
? "held"
|
|
69
|
+
: lock.state === "released"
|
|
70
|
+
? "released"
|
|
71
|
+
: "unknown",
|
|
72
|
+
nowMs: options.nowMs,
|
|
73
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
74
|
+
}),
|
|
75
|
+
records,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
life: {
|
|
80
|
+
phase: "unknown",
|
|
81
|
+
diagnostic: "generation history changed during runner-lock observation",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function observeProjectionLife(projectionDirectory, options) {
|
|
86
|
+
return observeProjectionLifeSnapshot(projectionDirectory, options).life;
|
|
87
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const GENERATION_VERDICT_STATES = [
|
|
2
|
+
"completed",
|
|
3
|
+
"failed",
|
|
4
|
+
"launch-failed",
|
|
5
|
+
"lost-runner",
|
|
6
|
+
"dismissed",
|
|
7
|
+
];
|
|
8
|
+
function asLaunch(record) {
|
|
9
|
+
return record.kind === "launch"
|
|
10
|
+
? record
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function asAdoption(record) {
|
|
14
|
+
return record.kind === "adoption"
|
|
15
|
+
? record
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
function asVerdict(record) {
|
|
19
|
+
return record.kind === "verdict"
|
|
20
|
+
? record
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Select the current generation using ledger sequence only. The store validates
|
|
25
|
+
* dense sequence and transition legality before records reach this fold.
|
|
26
|
+
*/
|
|
27
|
+
export function selectCurrentGeneration(records) {
|
|
28
|
+
let launch;
|
|
29
|
+
for (const record of records) {
|
|
30
|
+
const candidate = asLaunch(record);
|
|
31
|
+
if (candidate)
|
|
32
|
+
launch = candidate;
|
|
33
|
+
}
|
|
34
|
+
if (!launch)
|
|
35
|
+
return null;
|
|
36
|
+
let adoption;
|
|
37
|
+
let verdict;
|
|
38
|
+
for (const record of records) {
|
|
39
|
+
if (record.seq <= launch.seq || record.executionId !== launch.executionId)
|
|
40
|
+
continue;
|
|
41
|
+
adoption ??= asAdoption(record);
|
|
42
|
+
verdict ??= asVerdict(record);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
launch,
|
|
46
|
+
...(adoption ? { adoption } : {}),
|
|
47
|
+
...(verdict ? { verdict } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function verdictState(record) {
|
|
51
|
+
const state = record?.facts.state;
|
|
52
|
+
return typeof state === "string"
|
|
53
|
+
&& GENERATION_VERDICT_STATES.includes(state)
|
|
54
|
+
? state
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
function phaseForVerdict(state) {
|
|
58
|
+
switch (state) {
|
|
59
|
+
case "completed": return "done";
|
|
60
|
+
case "dismissed": return "dismissed";
|
|
61
|
+
case "lost-runner": return "lost";
|
|
62
|
+
case "failed":
|
|
63
|
+
case "launch-failed":
|
|
64
|
+
return "failed";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function deriveProjectionLife(input) {
|
|
68
|
+
const chain = selectCurrentGeneration(input.records);
|
|
69
|
+
if (!chain)
|
|
70
|
+
return { phase: "quiescent" };
|
|
71
|
+
const executionId = chain.launch.executionId;
|
|
72
|
+
if (chain.verdict) {
|
|
73
|
+
const state = verdictState(chain.verdict);
|
|
74
|
+
return state
|
|
75
|
+
? { phase: phaseForVerdict(state), executionId, verdict: state }
|
|
76
|
+
: { phase: "unknown", executionId, diagnostic: "current generation has an invalid verdict" };
|
|
77
|
+
}
|
|
78
|
+
if (!chain.adoption) {
|
|
79
|
+
const createdAt = chain.launch.facts.createdAt;
|
|
80
|
+
const launchedAtMs = typeof createdAt === "string" ? Date.parse(createdAt) : Number.NaN;
|
|
81
|
+
if (!Number.isFinite(launchedAtMs)) {
|
|
82
|
+
return { phase: "unknown", executionId, diagnostic: "current launch has an invalid createdAt fact" };
|
|
83
|
+
}
|
|
84
|
+
return input.nowMs - launchedAtMs >= input.startupTimeoutMs
|
|
85
|
+
? { phase: "startup-timeout", executionId }
|
|
86
|
+
: { phase: "minting", executionId };
|
|
87
|
+
}
|
|
88
|
+
switch (input.runnerLock) {
|
|
89
|
+
case "held": return { phase: "active", executionId };
|
|
90
|
+
case "released": return { phase: "lost", executionId };
|
|
91
|
+
case "unknown":
|
|
92
|
+
case undefined:
|
|
93
|
+
return { phase: "unknown", executionId, diagnostic: "runner lock could not be determined" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { isErrnoException } from "../errno.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { ProjectionStateError, randomHex8 } from "./atomic-publish.js";
|
|
5
|
+
import { isoFromNowMs, PROJECTION_ROOT_SEGMENTS, projectionDir, projectionRoot, } from "./projection-core.js";
|
|
6
|
+
import { parseExecutionAuthoritySelection, parseExecutionPact, parseExecutionPactFields, } from "./execution-pact.js";
|
|
7
|
+
import { openProjectionGenerationStore } from "./projection-generation-store.js";
|
|
8
|
+
import { initializeProjectionRunnerLockDatabase } from "./projection-runner-lock.js";
|
|
6
9
|
export class ProjectionMintCollisionError extends Error {
|
|
7
10
|
code = "PROJECTION_MINT_COLLISION";
|
|
8
11
|
constructor(message, options) {
|
|
@@ -22,11 +25,10 @@ function assertMintSlug(slug) {
|
|
|
22
25
|
return normalized;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Readers ignore directories without `ready`.
|
|
28
|
+
* Validate mint inputs that can fail, then reserve one private staging directory.
|
|
29
|
+
* Dot-prefixed staging is never a published projection address.
|
|
28
30
|
*/
|
|
29
|
-
|
|
31
|
+
function reserveProjectionDirectory(input) {
|
|
30
32
|
const slug = assertMintSlug(input.slug);
|
|
31
33
|
if (!Number.isInteger(input.pid) || input.pid < 0) {
|
|
32
34
|
throw new ProjectionStateError("projection pid must be a non-negative integer");
|
|
@@ -47,8 +49,11 @@ export function mintProjection(input) {
|
|
|
47
49
|
}
|
|
48
50
|
const id = `${slug}-${hex}`;
|
|
49
51
|
const dir = projectionDir(input.cwd, id);
|
|
52
|
+
const stagingDir = path.join(root, `.mint-${id}`);
|
|
53
|
+
if (fs.existsSync(dir))
|
|
54
|
+
continue;
|
|
50
55
|
try {
|
|
51
|
-
fs.mkdirSync(
|
|
56
|
+
fs.mkdirSync(stagingDir);
|
|
52
57
|
}
|
|
53
58
|
catch (error) {
|
|
54
59
|
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
@@ -56,24 +61,91 @@ export function mintProjection(input) {
|
|
|
56
61
|
}
|
|
57
62
|
throw error;
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
akuma: input.akuma,
|
|
63
|
-
provider: input.provider,
|
|
64
|
-
mintedAt: isoFromNowMs(input.nowMs),
|
|
65
|
-
...(input.effectivePolicy ? { effectivePolicy: input.effectivePolicy } : {}),
|
|
66
|
-
};
|
|
67
|
-
fs.writeFileSync(path.join(dir, "pact.json"), encodeProjectionJson(pact), "utf8");
|
|
68
|
-
fs.writeFileSync(path.join(dir, "heartbeat"), Buffer.alloc(0));
|
|
69
|
-
for (const layer of PROJECTION_LAYER_DIRS) {
|
|
70
|
-
fs.mkdirSync(path.join(dir, layer));
|
|
71
|
-
}
|
|
72
|
-
const ready = atomicPublishFile(path.join(dir, "ready"), Buffer.alloc(0));
|
|
73
|
-
if (ready === "taken") {
|
|
74
|
-
throw new ProjectionStateError(`ready marker already present while minting ${id}`);
|
|
64
|
+
if (fs.existsSync(dir)) {
|
|
65
|
+
fs.rmdirSync(stagingDir);
|
|
66
|
+
continue;
|
|
75
67
|
}
|
|
76
|
-
return { id, dir,
|
|
68
|
+
return { id, dir, stagingDir };
|
|
77
69
|
}
|
|
78
70
|
throw new ProjectionMintCollisionError(`exhausted ${maxAttempts} projection id attempts under ${PROJECTION_ROOT_SEGMENTS.join("/")} (PROJECTION_MINT_COLLISION)`);
|
|
79
71
|
}
|
|
72
|
+
/** Build the pact fields shared by both authority variants. */
|
|
73
|
+
function buildMintPactFields(input) {
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
pid: input.pid,
|
|
77
|
+
akuma: input.akuma,
|
|
78
|
+
provider: input.provider,
|
|
79
|
+
binding: input.binding,
|
|
80
|
+
mintedAt: isoFromNowMs(input.nowMs),
|
|
81
|
+
workspaceRoot: input.workspaceRoot ?? path.resolve(input.cwd),
|
|
82
|
+
...(input.effectivePolicy ? { effectivePolicy: input.effectivePolicy } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build both hard projection organs in private staging, close every database
|
|
87
|
+
* handle, then publish the complete projection with one directory rename.
|
|
88
|
+
*/
|
|
89
|
+
function publishReservedProjection(pact, reserved) {
|
|
90
|
+
let published = false;
|
|
91
|
+
try {
|
|
92
|
+
const heart = openProjectionGenerationStore(reserved.stagingDir);
|
|
93
|
+
try {
|
|
94
|
+
heart.initializeIdentity(pact);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
heart.close();
|
|
98
|
+
}
|
|
99
|
+
initializeProjectionRunnerLockDatabase(reserved.stagingDir);
|
|
100
|
+
fs.renameSync(reserved.stagingDir, reserved.dir);
|
|
101
|
+
published = true;
|
|
102
|
+
return { id: reserved.id, dir: reserved.dir, pact };
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
if (!published) {
|
|
106
|
+
fs.rmSync(reserved.stagingDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Mint a user-authority projection directory.
|
|
112
|
+
* Order (closed): validate → reserve staging → heart + leash → atomic rename.
|
|
113
|
+
*/
|
|
114
|
+
export function mintProjection(input) {
|
|
115
|
+
const authority = parseExecutionAuthoritySelection(input.authority, "projection identity", "user");
|
|
116
|
+
const fields = parseExecutionPactFields(buildMintPactFields(input));
|
|
117
|
+
const pact = parseExecutionPact({
|
|
118
|
+
version: fields.version,
|
|
119
|
+
pid: fields.pid,
|
|
120
|
+
akuma: fields.akuma,
|
|
121
|
+
provider: fields.provider,
|
|
122
|
+
authority,
|
|
123
|
+
binding: fields.binding,
|
|
124
|
+
mintedAt: fields.mintedAt,
|
|
125
|
+
workspaceRoot: fields.workspaceRoot,
|
|
126
|
+
...(fields.effectivePolicy !== undefined ? { effectivePolicy: fields.effectivePolicy } : {}),
|
|
127
|
+
});
|
|
128
|
+
const reserved = reserveProjectionDirectory(input);
|
|
129
|
+
return publishReservedProjection(pact, reserved);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Mint a repository-authority projection.
|
|
133
|
+
* Order: validate complete identity → reserve staging → heart + leash → atomic rename.
|
|
134
|
+
*/
|
|
135
|
+
export async function mintRepositoryProjection(input) {
|
|
136
|
+
parseExecutionAuthoritySelection(input.authority, "projection identity", "repository");
|
|
137
|
+
const fields = parseExecutionPactFields(buildMintPactFields(input));
|
|
138
|
+
const reserved = reserveProjectionDirectory(input);
|
|
139
|
+
const pact = parseExecutionPact({
|
|
140
|
+
version: fields.version,
|
|
141
|
+
pid: fields.pid,
|
|
142
|
+
akuma: fields.akuma,
|
|
143
|
+
provider: fields.provider,
|
|
144
|
+
authority: { kind: "repository" },
|
|
145
|
+
binding: fields.binding,
|
|
146
|
+
mintedAt: fields.mintedAt,
|
|
147
|
+
workspaceRoot: fields.workspaceRoot,
|
|
148
|
+
...(fields.effectivePolicy !== undefined ? { effectivePolicy: fields.effectivePolicy } : {}),
|
|
149
|
+
});
|
|
150
|
+
return publishReservedProjection(pact, reserved);
|
|
151
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
export const PROJECTION_RUNNER_LOCK_DATABASE = "leash";
|
|
5
|
+
const SQLITE_BUSY = 5;
|
|
6
|
+
const SUPPORTED_PLATFORMS = new Set(["darwin", "linux"]);
|
|
7
|
+
export class ProjectionRunnerLockUnavailableError extends Error {
|
|
8
|
+
code = "PROJECTION_RUNNER_LOCK_UNAVAILABLE";
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "ProjectionRunnerLockUnavailableError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class ProjectionRunnerLockUnsupportedError extends Error {
|
|
15
|
+
code = "PROJECTION_RUNNER_LOCK_UNSUPPORTED";
|
|
16
|
+
constructor(platform) {
|
|
17
|
+
super(`projection runner locks are unsupported on platform '${platform}'`);
|
|
18
|
+
this.name = "ProjectionRunnerLockUnsupportedError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function projectionRunnerLockPath(projectionDirectory) {
|
|
22
|
+
return path.join(projectionDirectory, PROJECTION_RUNNER_LOCK_DATABASE);
|
|
23
|
+
}
|
|
24
|
+
export function isProjectionRunnerLockPlatformSupported(platform = process.platform) {
|
|
25
|
+
return SUPPORTED_PLATFORMS.has(platform);
|
|
26
|
+
}
|
|
27
|
+
function errorDetail(error) {
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
30
|
+
function isSqliteBusy(error) {
|
|
31
|
+
return typeof error === "object"
|
|
32
|
+
&& error !== null
|
|
33
|
+
&& "errcode" in error
|
|
34
|
+
&& error.errcode === SQLITE_BUSY;
|
|
35
|
+
}
|
|
36
|
+
function readJournalMode(database) {
|
|
37
|
+
const row = database.prepare("PRAGMA journal_mode").get();
|
|
38
|
+
return typeof row?.journal_mode === "string" ? row.journal_mode.toLowerCase() : undefined;
|
|
39
|
+
}
|
|
40
|
+
function assertCurrentLockSchema(database) {
|
|
41
|
+
database.prepare("SELECT id FROM lock_anchor LIMIT 1").get();
|
|
42
|
+
}
|
|
43
|
+
function closeAfterFailedAcquire(database, error) {
|
|
44
|
+
try {
|
|
45
|
+
database.close();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// The acquisition error remains the causal failure. Closing a connection
|
|
49
|
+
// that never became authoritative cannot turn it into a held lock.
|
|
50
|
+
}
|
|
51
|
+
if (isSqliteBusy(error)) {
|
|
52
|
+
throw new ProjectionRunnerLockUnavailableError("projection runner lock is already held", { cause: error });
|
|
53
|
+
}
|
|
54
|
+
throw new ProjectionRunnerLockUnavailableError(`projection runner lock acquisition failed: ${errorDetail(error)}`, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Mint-time initialization for the fixed lock database. This creates storage,
|
|
58
|
+
* forces rollback-journal mode, verifies the anchor schema, and closes without
|
|
59
|
+
* retaining any process-life authority.
|
|
60
|
+
*/
|
|
61
|
+
export function initializeProjectionRunnerLockDatabase(projectionDirectory, platform = process.platform) {
|
|
62
|
+
if (!isProjectionRunnerLockPlatformSupported(platform)) {
|
|
63
|
+
throw new ProjectionRunnerLockUnsupportedError(platform);
|
|
64
|
+
}
|
|
65
|
+
const lockPath = projectionRunnerLockPath(projectionDirectory);
|
|
66
|
+
let database;
|
|
67
|
+
try {
|
|
68
|
+
database = new DatabaseSync(lockPath);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new ProjectionRunnerLockUnavailableError(`projection runner lock database could not be initialized: ${errorDetail(error)}`, { cause: error });
|
|
72
|
+
}
|
|
73
|
+
let initializationError;
|
|
74
|
+
try {
|
|
75
|
+
database.exec("PRAGMA busy_timeout=0");
|
|
76
|
+
database.exec("PRAGMA journal_mode=DELETE");
|
|
77
|
+
database.exec("CREATE TABLE IF NOT EXISTS lock_anchor (id INTEGER PRIMARY KEY CHECK (id = 1))");
|
|
78
|
+
if (readJournalMode(database) !== "delete") {
|
|
79
|
+
throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
|
|
80
|
+
}
|
|
81
|
+
assertCurrentLockSchema(database);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
initializationError = error;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
database.close();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
initializationError ??= error;
|
|
91
|
+
}
|
|
92
|
+
if (initializationError !== undefined) {
|
|
93
|
+
throw new ProjectionRunnerLockUnavailableError(`projection runner lock database initialization failed: ${errorDetail(initializationError)}`, { cause: initializationError });
|
|
94
|
+
}
|
|
95
|
+
return lockPath;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Open the already initialized projection-local lock database and hold one
|
|
99
|
+
* rollback-mode EXCLUSIVE transaction until close() or process death. SQLite
|
|
100
|
+
* requires a writable connection for this write-grade lock; the preceding
|
|
101
|
+
* regular-file check prevents ordinary missing storage from being created.
|
|
102
|
+
* Database bytes and timestamps are deliberately absent from the returned authority.
|
|
103
|
+
*/
|
|
104
|
+
export function acquireProjectionRunnerLock(projectionDirectory, platform = process.platform) {
|
|
105
|
+
if (!isProjectionRunnerLockPlatformSupported(platform)) {
|
|
106
|
+
throw new ProjectionRunnerLockUnsupportedError(platform);
|
|
107
|
+
}
|
|
108
|
+
const lockPath = projectionRunnerLockPath(projectionDirectory);
|
|
109
|
+
try {
|
|
110
|
+
if (!fs.lstatSync(lockPath).isFile()) {
|
|
111
|
+
throw new ProjectionRunnerLockUnavailableError("projection runner lock database is not a regular file");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof ProjectionRunnerLockUnavailableError)
|
|
116
|
+
throw error;
|
|
117
|
+
throw new ProjectionRunnerLockUnavailableError(`projection runner lock database is not initialized: ${errorDetail(error)}`, { cause: error });
|
|
118
|
+
}
|
|
119
|
+
let database;
|
|
120
|
+
try {
|
|
121
|
+
database = new DatabaseSync(lockPath);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw new ProjectionRunnerLockUnavailableError(`projection runner lock database could not be opened: ${errorDetail(error)}`, { cause: error });
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
database.exec("PRAGMA busy_timeout=0");
|
|
128
|
+
database.exec("BEGIN EXCLUSIVE");
|
|
129
|
+
if (readJournalMode(database) !== "delete") {
|
|
130
|
+
throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
|
|
131
|
+
}
|
|
132
|
+
assertCurrentLockSchema(database);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
try {
|
|
136
|
+
database.exec("ROLLBACK");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// A failure before BEGIN has no transaction; a failure after BEGIN is
|
|
140
|
+
// still released by closeAfterFailedAcquire if rollback cannot run.
|
|
141
|
+
}
|
|
142
|
+
return closeAfterFailedAcquire(database, error);
|
|
143
|
+
}
|
|
144
|
+
let closed = false;
|
|
145
|
+
return {
|
|
146
|
+
path: lockPath,
|
|
147
|
+
close() {
|
|
148
|
+
if (closed)
|
|
149
|
+
return;
|
|
150
|
+
closed = true;
|
|
151
|
+
let rollbackError;
|
|
152
|
+
try {
|
|
153
|
+
database.exec("ROLLBACK");
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
rollbackError = error;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
database.close();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
rollbackError ??= error;
|
|
163
|
+
}
|
|
164
|
+
if (rollbackError !== undefined) {
|
|
165
|
+
throw new ProjectionRunnerLockUnavailableError(`projection runner lock release failed: ${errorDetail(rollbackError)}`, { cause: rollbackError });
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Probe without creating the lock database. BEGIN DEFERRED itself acquires no
|
|
172
|
+
* file lock, so the sqlite_schema SELECT is mandatory: SQLITE_BUSY there is
|
|
173
|
+
* positive evidence of the runner's EXCLUSIVE transaction. Successful reads
|
|
174
|
+
* hold a compatible SHARED transaction only long enough to verify DELETE
|
|
175
|
+
* journal mode, then roll back. Every other failure remains unknown.
|
|
176
|
+
*/
|
|
177
|
+
export function observeProjectionRunnerLock(projectionDirectory, platform = process.platform) {
|
|
178
|
+
if (!isProjectionRunnerLockPlatformSupported(platform)) {
|
|
179
|
+
return {
|
|
180
|
+
state: "unknown",
|
|
181
|
+
reason: "unsupported-platform",
|
|
182
|
+
detail: `projection runner locks are unsupported on platform '${platform}'`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const lockPath = projectionRunnerLockPath(projectionDirectory);
|
|
186
|
+
let database;
|
|
187
|
+
try {
|
|
188
|
+
database = new DatabaseSync(lockPath, { readOnly: true });
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
return { state: "unknown", reason: "open-failed", detail: errorDetail(error) };
|
|
192
|
+
}
|
|
193
|
+
let transactionOpen = false;
|
|
194
|
+
try {
|
|
195
|
+
database.exec("PRAGMA busy_timeout=0");
|
|
196
|
+
database.exec("BEGIN DEFERRED");
|
|
197
|
+
transactionOpen = true;
|
|
198
|
+
assertCurrentLockSchema(database);
|
|
199
|
+
const journalMode = readJournalMode(database);
|
|
200
|
+
if (journalMode !== "delete") {
|
|
201
|
+
return {
|
|
202
|
+
state: "unknown",
|
|
203
|
+
reason: "unsupported-journal-mode",
|
|
204
|
+
detail: `projection runner lock database uses journal mode '${journalMode ?? "unknown"}'`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return { state: "released" };
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (isSqliteBusy(error))
|
|
211
|
+
return { state: "held" };
|
|
212
|
+
return { state: "unknown", reason: "probe-failed", detail: errorDetail(error) };
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
if (transactionOpen) {
|
|
216
|
+
try {
|
|
217
|
+
database.exec("ROLLBACK");
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Closing below still drops this observer's own ephemeral SHARED lock.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
database.close();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Observer cleanup failures cannot be converted into death evidence.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|