@astrosheep/keiyaku 2.8.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms_v2.js +6 -2
- package/build/agents/harness/index.js +5 -1
- package/build/agents/harness/pump.js +22 -5
- package/build/agents/launch-snapshot_v2.js +14 -9
- package/build/agents/selector_v2.js +15 -3
- package/build/cli/commands/akuma.js +21 -3
- package/build/cli/completion.js +4 -1
- package/build/cli/flags.js +10 -1
- package/build/cli/help.js +3 -3
- package/build/cli/index.js +45 -14
- package/build/cli/parse.js +60 -24
- package/build/cli/projection-address.js +97 -10
- package/build/cli/render/address.js +12 -3
- package/build/cli/render/call.js +10 -5
- package/build/cli/render/shared.js +2 -2
- package/build/cli/render/status.js +19 -4
- package/build/cli/render/wait.js +3 -3
- package/build/config/provider-policy-ownership.js +39 -0
- package/build/config/settings.js +1 -11
- package/build/core/addressing.js +82 -38
- package/build/core/call.js +33 -5
- package/build/core/projection/mint.js +23 -3
- package/build/core/projection-alias.js +123 -0
- package/build/core/projection-coordinate.js +23 -7
- package/build/core/projection-core.js +8 -47
- package/build/core/projection-mint.js +24 -3
- package/build/core/projection-runner-lock.js +1 -1
- package/build/core/projection-status.js +111 -12
- package/build/core/response-artifact-id.js +5 -0
- package/build/core/status.js +1 -1
- package/build/core/transcripts.js +42 -15
- package/build/generated/version.js +1 -1
- package/package.json +4 -4
|
@@ -20,11 +20,21 @@ function assertMintSlug(slug) {
|
|
|
20
20
|
if (!normalized) {
|
|
21
21
|
throw new ProjectionStateError("projection slug cannot be empty");
|
|
22
22
|
}
|
|
23
|
-
if (
|
|
23
|
+
if (!/^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*$/.test(normalized)) {
|
|
24
24
|
throw new ProjectionStateError(`invalid projection slug: ${slug}`);
|
|
25
25
|
}
|
|
26
26
|
return normalized;
|
|
27
27
|
}
|
|
28
|
+
function removeEmptyAkumaRoot(dir) {
|
|
29
|
+
try {
|
|
30
|
+
fs.rmdirSync(dir);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (isErrnoException(error) && ["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
34
|
+
return;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
28
38
|
/**
|
|
29
39
|
* Validate mint inputs that can fail, then reserve one private staging directory.
|
|
30
40
|
* Dot-prefixed staging is never a published projection address.
|
|
@@ -43,14 +53,19 @@ function reserveProjectionDirectory(input) {
|
|
|
43
53
|
}
|
|
44
54
|
const root = projectionRoot(input.cwd);
|
|
45
55
|
fs.mkdirSync(root, { recursive: true });
|
|
56
|
+
const akumaRoot = path.join(root, slug);
|
|
57
|
+
const createdAkumaRoot = !fs.existsSync(akumaRoot);
|
|
58
|
+
fs.mkdirSync(akumaRoot, { recursive: true });
|
|
46
59
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
47
60
|
const hex = (input.randomHex?.(attempt) ?? randomHex8()).toLowerCase();
|
|
48
61
|
if (!/^[0-9a-f]{8}$/.test(hex)) {
|
|
62
|
+
if (createdAkumaRoot)
|
|
63
|
+
removeEmptyAkumaRoot(akumaRoot);
|
|
49
64
|
throw new ProjectionStateError(`projection id random segment must be 8 hex chars, got ${hex}`);
|
|
50
65
|
}
|
|
51
|
-
const id = `${slug}
|
|
66
|
+
const id = `${slug}/${hex}`;
|
|
52
67
|
const dir = projectionDir(input.cwd, id);
|
|
53
|
-
const stagingDir = path.join(
|
|
68
|
+
const stagingDir = path.join(akumaRoot, `.mint-${hex}`);
|
|
54
69
|
if (fs.existsSync(dir))
|
|
55
70
|
continue;
|
|
56
71
|
try {
|
|
@@ -60,6 +75,8 @@ function reserveProjectionDirectory(input) {
|
|
|
60
75
|
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
61
76
|
continue;
|
|
62
77
|
}
|
|
78
|
+
if (createdAkumaRoot)
|
|
79
|
+
removeEmptyAkumaRoot(akumaRoot);
|
|
63
80
|
throw error;
|
|
64
81
|
}
|
|
65
82
|
if (fs.existsSync(dir)) {
|
|
@@ -68,6 +85,8 @@ function reserveProjectionDirectory(input) {
|
|
|
68
85
|
}
|
|
69
86
|
return { id, dir, stagingDir };
|
|
70
87
|
}
|
|
88
|
+
if (createdAkumaRoot)
|
|
89
|
+
removeEmptyAkumaRoot(akumaRoot);
|
|
71
90
|
throw new ProjectionMintCollisionError(`exhausted ${maxAttempts} projection id attempts under ${PROJECTION_ROOT_SEGMENTS.join("/")} (PROJECTION_MINT_COLLISION)`);
|
|
72
91
|
}
|
|
73
92
|
/** Build the identity fields shared by both authority variants. */
|
|
@@ -107,6 +126,7 @@ function publishReservedProjection(pact, reserved) {
|
|
|
107
126
|
if (!published) {
|
|
108
127
|
// Failure removes only this mint's staging.
|
|
109
128
|
fs.rmSync(reserved.stagingDir, { recursive: true, force: true });
|
|
129
|
+
removeEmptyAkumaRoot(path.dirname(reserved.stagingDir));
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isErrnoException } from "../errno.js";
|
|
4
|
+
import { ProjectionStateError, randomHex8 } from "./atomic-publish.js";
|
|
5
|
+
import { projectionCoordinateRoot } from "./projection-coordinate.js";
|
|
6
|
+
import { isProjectionId } from "./projection-core.js";
|
|
7
|
+
export const PROJECTION_ALIAS_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
8
|
+
const MAX_ALIAS_REF_BYTES = 1024;
|
|
9
|
+
export function isProjectionAlias(value) {
|
|
10
|
+
return PROJECTION_ALIAS_PATTERN.test(value);
|
|
11
|
+
}
|
|
12
|
+
export function assertProjectionAlias(value) {
|
|
13
|
+
if (!isProjectionAlias(value)) {
|
|
14
|
+
throw new ProjectionStateError("projection alias must match ^[a-z0-9][a-z0-9-]{0,63}$");
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
export function projectionAliasRoot(cwd) {
|
|
19
|
+
return path.join(projectionCoordinateRoot(cwd), ".keiyaku", "projection-alias");
|
|
20
|
+
}
|
|
21
|
+
export function projectionAliasPath(cwd, alias) {
|
|
22
|
+
assertProjectionAlias(alias);
|
|
23
|
+
return path.join(projectionAliasRoot(cwd), alias);
|
|
24
|
+
}
|
|
25
|
+
function invalidAliasRef(filePath) {
|
|
26
|
+
return new ProjectionStateError(`invalid projection alias ref: ${filePath}`);
|
|
27
|
+
}
|
|
28
|
+
function readBoundedRegularAliasRef(filePath) {
|
|
29
|
+
let before;
|
|
30
|
+
try {
|
|
31
|
+
before = fs.lstatSync(filePath);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
35
|
+
return undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
if (!before.isFile() || before.size > MAX_ALIAS_REF_BYTES)
|
|
39
|
+
throw invalidAliasRef(filePath);
|
|
40
|
+
let fd;
|
|
41
|
+
try {
|
|
42
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (isErrnoException(error) && error.code === "ELOOP")
|
|
46
|
+
throw invalidAliasRef(filePath);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const opened = fs.fstatSync(fd);
|
|
51
|
+
if (!opened.isFile() || opened.size > MAX_ALIAS_REF_BYTES)
|
|
52
|
+
throw invalidAliasRef(filePath);
|
|
53
|
+
const bytes = Buffer.alloc(opened.size);
|
|
54
|
+
const count = fs.readSync(fd, bytes, 0, bytes.length, 0);
|
|
55
|
+
if (count !== bytes.length)
|
|
56
|
+
throw invalidAliasRef(filePath);
|
|
57
|
+
return bytes.toString("utf8");
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
fs.closeSync(fd);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Undefined means no ref; malformed refs fail closed with their exact sidecar path. */
|
|
64
|
+
export function readProjectionAlias(cwd, alias) {
|
|
65
|
+
const filePath = projectionAliasPath(cwd, alias);
|
|
66
|
+
const content = readBoundedRegularAliasRef(filePath);
|
|
67
|
+
if (content === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (!content.endsWith("\n") || content.indexOf("\n") !== content.length - 1)
|
|
70
|
+
throw invalidAliasRef(filePath);
|
|
71
|
+
const id = content.slice(0, -1);
|
|
72
|
+
if (!isProjectionId(id))
|
|
73
|
+
throw invalidAliasRef(filePath);
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
/** Atomically replace one alias ref. Aliases intentionally have no multi-ref transaction. */
|
|
77
|
+
export function moveProjectionAlias(cwd, alias, id) {
|
|
78
|
+
assertProjectionAlias(alias);
|
|
79
|
+
if (!isProjectionId(id))
|
|
80
|
+
throw new ProjectionStateError(`invalid projection id: ${id}`);
|
|
81
|
+
const previous = readProjectionAlias(cwd, alias);
|
|
82
|
+
const root = projectionAliasRoot(cwd);
|
|
83
|
+
fs.mkdirSync(root, { recursive: true });
|
|
84
|
+
const target = projectionAliasPath(cwd, alias);
|
|
85
|
+
let temp;
|
|
86
|
+
try {
|
|
87
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
88
|
+
const candidate = path.join(root, `.tmp.${randomHex8()}`);
|
|
89
|
+
try {
|
|
90
|
+
const fd = fs.openSync(candidate, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
91
|
+
temp = candidate;
|
|
92
|
+
try {
|
|
93
|
+
fs.writeFileSync(fd, `${id}\n`, "utf8");
|
|
94
|
+
fs.fsyncSync(fd);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (isErrnoException(error) && error.code === "EEXIST")
|
|
103
|
+
continue;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!temp)
|
|
108
|
+
throw new ProjectionStateError(`failed to create projection alias temp: ${target}`);
|
|
109
|
+
fs.renameSync(temp, target);
|
|
110
|
+
const directoryFd = fs.openSync(root, fs.constants.O_RDONLY);
|
|
111
|
+
try {
|
|
112
|
+
fs.fsyncSync(directoryFd);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
fs.closeSync(directoryFd);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
if (temp)
|
|
120
|
+
fs.rmSync(temp, { force: true });
|
|
121
|
+
}
|
|
122
|
+
return previous;
|
|
123
|
+
}
|
|
@@ -2,13 +2,13 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { ProjectionStateError } from "./atomic-publish.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Resolve projection storage root and authority from one Git probe.
|
|
6
6
|
*
|
|
7
7
|
* A non-bare repository uses the parent of its absolute common Git directory,
|
|
8
8
|
* so every linked worktree shares one coordinate. Repo-free callers retain the
|
|
9
9
|
* existing cwd-local authority until its replacement is legislated.
|
|
10
10
|
*/
|
|
11
|
-
export function
|
|
11
|
+
export function resolveProjectionCoordinate(cwd) {
|
|
12
12
|
const result = spawnSync("git", ["rev-parse", "--is-bare-repository", "--path-format=absolute", "--git-common-dir"], {
|
|
13
13
|
cwd,
|
|
14
14
|
encoding: "utf8",
|
|
@@ -24,12 +24,28 @@ export function projectionCoordinateRoot(cwd) {
|
|
|
24
24
|
}
|
|
25
25
|
if (result.status !== 0) {
|
|
26
26
|
const detail = result.stderr.trim();
|
|
27
|
-
if (detail.includes("not a git repository"))
|
|
28
|
-
return cwd;
|
|
27
|
+
if (detail.includes("not a git repository")) {
|
|
28
|
+
return { physicalRoot: cwd, authority: { kind: "user" } };
|
|
29
|
+
}
|
|
29
30
|
throw new ProjectionStateError(`cannot resolve projection repository coordinate${detail ? `: ${detail}` : ` (git exited ${String(result.status)})`}`);
|
|
30
31
|
}
|
|
31
32
|
const [bare, commonDir] = result.stdout.trim().split(/\r?\n/);
|
|
32
|
-
if (bare !== "false" || !commonDir || !path.isAbsolute(commonDir))
|
|
33
|
-
return cwd;
|
|
34
|
-
|
|
33
|
+
if (bare !== "false" || !commonDir || !path.isAbsolute(commonDir)) {
|
|
34
|
+
return { physicalRoot: cwd, authority: { kind: "user" } };
|
|
35
|
+
}
|
|
36
|
+
const physicalRoot = path.dirname(commonDir);
|
|
37
|
+
return {
|
|
38
|
+
physicalRoot,
|
|
39
|
+
authority: { kind: "repository", stableRoot: physicalRoot },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Select the filesystem authority for repository projection state.
|
|
44
|
+
*
|
|
45
|
+
* A non-bare repository uses the parent of its absolute common Git directory,
|
|
46
|
+
* so every linked worktree shares one coordinate. Repo-free callers retain the
|
|
47
|
+
* existing cwd-local authority until its replacement is legislated.
|
|
48
|
+
*/
|
|
49
|
+
export function projectionCoordinateRoot(cwd) {
|
|
50
|
+
return resolveProjectionCoordinate(cwd).physicalRoot;
|
|
35
51
|
}
|
|
@@ -3,16 +3,22 @@ import * as path from "node:path";
|
|
|
3
3
|
import { isErrnoException } from "../errno.js";
|
|
4
4
|
import { projectionCoordinateRoot } from "./projection-coordinate.js";
|
|
5
5
|
import { ProjectionStateError, } from "./atomic-publish.js";
|
|
6
|
-
import { readProjectionIdentity } from "./projection-identity.js";
|
|
7
6
|
export { atomicPublishFile, ProjectionStateError, randomHex8 } from "./atomic-publish.js";
|
|
8
7
|
export { claimAllInbox, claimFencedTells, claimTellToInflight, demoteSubmittedTellsForReplay, isTellCausallyConsumed, listTellIds, markTellConsumed, markTellSubmitted, readTellOriginal, readTellSubmitted, snapshotPendingTells, writeInboxTell, } from "./projection-tells.js";
|
|
9
8
|
/** Relative segments for `.keiyaku/projection`. */
|
|
10
9
|
export const PROJECTION_ROOT_SEGMENTS = [".keiyaku", "projection"];
|
|
10
|
+
export const PROJECTION_ID_PATTERN = /^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*\/[0-9a-f]{8}$/;
|
|
11
|
+
export function isProjectionId(value) {
|
|
12
|
+
return PROJECTION_ID_PATTERN.test(value);
|
|
13
|
+
}
|
|
11
14
|
export function projectionRoot(cwd) {
|
|
12
15
|
return path.join(projectionCoordinateRoot(cwd), ...PROJECTION_ROOT_SEGMENTS);
|
|
13
16
|
}
|
|
14
17
|
export function projectionDir(cwd, id) {
|
|
15
|
-
|
|
18
|
+
if (!isProjectionId(id))
|
|
19
|
+
throw new ProjectionStateError(`invalid projection id: ${id}`);
|
|
20
|
+
const [akuma, hex] = id.split("/");
|
|
21
|
+
return path.join(projectionRoot(cwd), akuma, hex);
|
|
16
22
|
}
|
|
17
23
|
export function encodeProjectionJson(value) {
|
|
18
24
|
return `${JSON.stringify(value)}\n`;
|
|
@@ -32,51 +38,6 @@ export function isPublishedProjection(dir) {
|
|
|
32
38
|
return false;
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
|
-
/** readdir-based address resolution. Atomic mint rename makes every visible non-dot directory published. */
|
|
36
|
-
export function resolveProjectionPrefix(cwd, prefix, commissionId) {
|
|
37
|
-
const needle = prefix.trim();
|
|
38
|
-
if (!needle) {
|
|
39
|
-
return { status: "none" };
|
|
40
|
-
}
|
|
41
|
-
const root = projectionRoot(cwd);
|
|
42
|
-
let entries;
|
|
43
|
-
try {
|
|
44
|
-
entries = fs.readdirSync(root);
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
48
|
-
return { status: "none" };
|
|
49
|
-
}
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
const publishedIds = entries
|
|
53
|
-
.filter((id) => {
|
|
54
|
-
if (id.startsWith("."))
|
|
55
|
-
return false;
|
|
56
|
-
const directory = path.join(root, id);
|
|
57
|
-
return isPublishedProjection(directory);
|
|
58
|
-
})
|
|
59
|
-
.sort();
|
|
60
|
-
const addressed = publishedIds.filter((id) => {
|
|
61
|
-
if (id === needle || id.startsWith(needle))
|
|
62
|
-
return true;
|
|
63
|
-
const suffix = /-([0-9a-f]{8})$/.exec(id)?.[1];
|
|
64
|
-
return suffix === needle;
|
|
65
|
-
});
|
|
66
|
-
const candidates = commissionId === undefined
|
|
67
|
-
? addressed
|
|
68
|
-
: addressed.filter((id) => {
|
|
69
|
-
const identity = readProjectionIdentity(path.join(root, id));
|
|
70
|
-
return identity.binding.kind === "commission" && identity.binding.commissionId === commissionId;
|
|
71
|
-
});
|
|
72
|
-
if (candidates.length === 1) {
|
|
73
|
-
return { status: "resolved", id: candidates[0] };
|
|
74
|
-
}
|
|
75
|
-
if (candidates.length === 0) {
|
|
76
|
-
return { status: "none" };
|
|
77
|
-
}
|
|
78
|
-
return { status: "ambiguous", candidates };
|
|
79
|
-
}
|
|
80
41
|
const PROJECTION_EXECUTION_ID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
81
42
|
function assertProjectionExecutionId(executionId) {
|
|
82
43
|
if (!PROJECTION_EXECUTION_ID_PATTERN.test(executionId)) {
|
|
@@ -19,11 +19,21 @@ function assertMintSlug(slug) {
|
|
|
19
19
|
if (!normalized) {
|
|
20
20
|
throw new ProjectionStateError("projection slug cannot be empty");
|
|
21
21
|
}
|
|
22
|
-
if (
|
|
22
|
+
if (!/^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*$/.test(normalized)) {
|
|
23
23
|
throw new ProjectionStateError(`invalid projection slug: ${slug}`);
|
|
24
24
|
}
|
|
25
25
|
return normalized;
|
|
26
26
|
}
|
|
27
|
+
function removeEmptyAkumaRoot(dir) {
|
|
28
|
+
try {
|
|
29
|
+
fs.rmdirSync(dir);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isErrnoException(error) && ["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
33
|
+
return;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
27
37
|
/**
|
|
28
38
|
* Validate mint inputs that can fail, then reserve one private staging directory.
|
|
29
39
|
* Dot-prefixed staging is never a published projection address.
|
|
@@ -42,14 +52,19 @@ function reserveProjectionDirectory(input) {
|
|
|
42
52
|
}
|
|
43
53
|
const root = projectionRoot(input.cwd);
|
|
44
54
|
fs.mkdirSync(root, { recursive: true });
|
|
55
|
+
const akumaRoot = path.join(root, slug);
|
|
56
|
+
const createdAkumaRoot = !fs.existsSync(akumaRoot);
|
|
57
|
+
fs.mkdirSync(akumaRoot, { recursive: true });
|
|
45
58
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
46
59
|
const hex = (input.randomHex?.(attempt) ?? randomHex8()).toLowerCase();
|
|
47
60
|
if (!/^[0-9a-f]{8}$/.test(hex)) {
|
|
61
|
+
if (createdAkumaRoot)
|
|
62
|
+
removeEmptyAkumaRoot(akumaRoot);
|
|
48
63
|
throw new ProjectionStateError(`projection id random segment must be 8 hex chars, got ${hex}`);
|
|
49
64
|
}
|
|
50
|
-
const id = `${slug}
|
|
65
|
+
const id = `${slug}/${hex}`;
|
|
51
66
|
const dir = projectionDir(input.cwd, id);
|
|
52
|
-
const stagingDir = path.join(
|
|
67
|
+
const stagingDir = path.join(akumaRoot, `.mint-${hex}`);
|
|
53
68
|
if (fs.existsSync(dir))
|
|
54
69
|
continue;
|
|
55
70
|
try {
|
|
@@ -59,6 +74,8 @@ function reserveProjectionDirectory(input) {
|
|
|
59
74
|
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
60
75
|
continue;
|
|
61
76
|
}
|
|
77
|
+
if (createdAkumaRoot)
|
|
78
|
+
removeEmptyAkumaRoot(akumaRoot);
|
|
62
79
|
throw error;
|
|
63
80
|
}
|
|
64
81
|
if (fs.existsSync(dir)) {
|
|
@@ -67,6 +84,8 @@ function reserveProjectionDirectory(input) {
|
|
|
67
84
|
}
|
|
68
85
|
return { id, dir, stagingDir };
|
|
69
86
|
}
|
|
87
|
+
if (createdAkumaRoot)
|
|
88
|
+
removeEmptyAkumaRoot(akumaRoot);
|
|
70
89
|
throw new ProjectionMintCollisionError(`exhausted ${maxAttempts} projection id attempts under ${PROJECTION_ROOT_SEGMENTS.join("/")} (PROJECTION_MINT_COLLISION)`);
|
|
71
90
|
}
|
|
72
91
|
/** Build the pact fields shared by both authority variants. */
|
|
@@ -104,6 +123,8 @@ function publishReservedProjection(pact, reserved) {
|
|
|
104
123
|
finally {
|
|
105
124
|
if (!published) {
|
|
106
125
|
fs.rmSync(reserved.stagingDir, { recursive: true, force: true });
|
|
126
|
+
const parent = path.dirname(reserved.stagingDir);
|
|
127
|
+
removeEmptyAkumaRoot(parent);
|
|
107
128
|
}
|
|
108
129
|
}
|
|
109
130
|
}
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
export const PROJECTION_RUNNER_LOCK_DATABASE = "leash";
|
|
5
5
|
const SQLITE_BUSY = 5;
|
|
6
|
-
const SUPPORTED_PLATFORMS = new Set(["darwin", "linux"]);
|
|
6
|
+
const SUPPORTED_PLATFORMS = new Set(["darwin", "linux", "win32"]);
|
|
7
7
|
export class ProjectionRunnerLockUnavailableError extends Error {
|
|
8
8
|
code = "PROJECTION_RUNNER_LOCK_UNAVAILABLE";
|
|
9
9
|
constructor(message, options) {
|
|
@@ -1,7 +1,8 @@
|
|
|
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 { executionEventsPath, projectionRoot, } from "./projection-core.js";
|
|
4
|
+
import { executionEventsPath, isProjectionId, projectionRoot, } from "./projection-core.js";
|
|
5
|
+
import { readProjectionAlias } from "./projection-alias.js";
|
|
5
6
|
import { ProjectionStateError } from "./atomic-publish.js";
|
|
6
7
|
import { foldAgentActivity, } from "./projection-activity.js";
|
|
7
8
|
import { readProjectionIdentity } from "./projection-identity.js";
|
|
@@ -12,6 +13,73 @@ import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
|
|
|
12
13
|
import { selectCurrentGeneration } from "./projection-life-protocol.js";
|
|
13
14
|
import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents } from "./stored-agent-event.js";
|
|
14
15
|
const MAX_STATUS_FILE_BYTES = 256 * 1024;
|
|
16
|
+
export const DEFAULT_PROJECTION_STATUS_ROW_BUDGET = 20;
|
|
17
|
+
const STATUS_GROUP = {
|
|
18
|
+
"startup-timeout": 0,
|
|
19
|
+
lost: 0,
|
|
20
|
+
unknown: 0,
|
|
21
|
+
minting: 1,
|
|
22
|
+
out: 1,
|
|
23
|
+
failed: 2,
|
|
24
|
+
dead: 2,
|
|
25
|
+
done: 3,
|
|
26
|
+
dismissed: 4,
|
|
27
|
+
};
|
|
28
|
+
function isOmissibleProjectionStatusState(state) {
|
|
29
|
+
return state === "failed" || state === "dead" || state === "done" || state === "dismissed";
|
|
30
|
+
}
|
|
31
|
+
function rowSortTime(row) {
|
|
32
|
+
if (row.activityAtMs !== undefined && Number.isFinite(row.activityAtMs)) {
|
|
33
|
+
return row.activityAtMs;
|
|
34
|
+
}
|
|
35
|
+
if (row.mintedAt !== undefined) {
|
|
36
|
+
const mintedAtMs = Date.parse(row.mintedAt);
|
|
37
|
+
if (Number.isFinite(mintedAtMs))
|
|
38
|
+
return mintedAtMs;
|
|
39
|
+
}
|
|
40
|
+
return Number.NEGATIVE_INFINITY;
|
|
41
|
+
}
|
|
42
|
+
function compareProjectionStatusRows(a, b) {
|
|
43
|
+
const groupDifference = STATUS_GROUP[a.state] - STATUS_GROUP[b.state];
|
|
44
|
+
if (groupDifference !== 0)
|
|
45
|
+
return groupDifference;
|
|
46
|
+
const aTime = rowSortTime(a);
|
|
47
|
+
const bTime = rowSortTime(b);
|
|
48
|
+
if (aTime !== bTime) {
|
|
49
|
+
if (aTime === Number.NEGATIVE_INFINITY)
|
|
50
|
+
return 1;
|
|
51
|
+
if (bTime === Number.NEGATIVE_INFINITY)
|
|
52
|
+
return -1;
|
|
53
|
+
return bTime - aTime;
|
|
54
|
+
}
|
|
55
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
56
|
+
}
|
|
57
|
+
export function prioritizeProjectionStatusRows(rows, showAll) {
|
|
58
|
+
const ordered = [...rows].sort(compareProjectionStatusRows);
|
|
59
|
+
if (showAll)
|
|
60
|
+
return { rows: ordered };
|
|
61
|
+
const protectedCount = ordered.filter((row) => STATUS_GROUP[row.state] < 2).length;
|
|
62
|
+
let terminalBudget = Math.max(0, DEFAULT_PROJECTION_STATUS_ROW_BUDGET - protectedCount);
|
|
63
|
+
const visible = [];
|
|
64
|
+
const byState = {
|
|
65
|
+
failed: 0,
|
|
66
|
+
dead: 0,
|
|
67
|
+
done: 0,
|
|
68
|
+
dismissed: 0,
|
|
69
|
+
};
|
|
70
|
+
for (const row of ordered) {
|
|
71
|
+
if (STATUS_GROUP[row.state] < 2 || terminalBudget > 0) {
|
|
72
|
+
visible.push(row);
|
|
73
|
+
if (STATUS_GROUP[row.state] >= 2)
|
|
74
|
+
terminalBudget -= 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (isOmissibleProjectionStatusState(row.state))
|
|
78
|
+
byState[row.state] += 1;
|
|
79
|
+
}
|
|
80
|
+
const total = Object.values(byState).reduce((sum, count) => sum + count, 0);
|
|
81
|
+
return total > 0 ? { rows: visible, omitted: { total, byState } } : { rows: visible };
|
|
82
|
+
}
|
|
15
83
|
class ProjectionStatusFileError extends Error {
|
|
16
84
|
diagnostic;
|
|
17
85
|
constructor(kind, filePath) {
|
|
@@ -198,7 +266,7 @@ function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallb
|
|
|
198
266
|
: {}),
|
|
199
267
|
};
|
|
200
268
|
}
|
|
201
|
-
function readProjectionRow(projectionDir, id, nowMs) {
|
|
269
|
+
function readProjectionRow(projectionDir, id, alias, nowMs) {
|
|
202
270
|
const diagnostics = [];
|
|
203
271
|
const identityFacts = readIdentityFacts(projectionDir, diagnostics);
|
|
204
272
|
const tellCounts = identityFacts.identity
|
|
@@ -232,6 +300,7 @@ function readProjectionRow(projectionDir, id, nowMs) {
|
|
|
232
300
|
const state = phase === "active" || phase === "quiescent" ? "out" : phase;
|
|
233
301
|
return {
|
|
234
302
|
id,
|
|
303
|
+
...(alias ? { alias } : {}),
|
|
235
304
|
state,
|
|
236
305
|
akuma: identityFacts.akuma,
|
|
237
306
|
provider: identityFacts.provider,
|
|
@@ -243,7 +312,7 @@ function readProjectionRow(projectionDir, id, nowMs) {
|
|
|
243
312
|
diagnostics,
|
|
244
313
|
};
|
|
245
314
|
}
|
|
246
|
-
export function readProjectionStatusBoard(cwd, nowMs) {
|
|
315
|
+
export function readProjectionStatusBoard(cwd, nowMs, options = {}) {
|
|
247
316
|
const root = projectionRoot(cwd);
|
|
248
317
|
try {
|
|
249
318
|
if (!fs.lstatSync(root).isDirectory())
|
|
@@ -263,17 +332,47 @@ export function readProjectionStatusBoard(cwd, nowMs) {
|
|
|
263
332
|
return null;
|
|
264
333
|
throw error;
|
|
265
334
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
335
|
+
const aliases = new Map();
|
|
336
|
+
const aliasRoot = path.join(path.dirname(path.dirname(root)), ".keiyaku", "projection-alias");
|
|
337
|
+
try {
|
|
338
|
+
for (const alias of fs.readdirSync(aliasRoot).sort()) {
|
|
339
|
+
if (alias.startsWith("."))
|
|
340
|
+
continue;
|
|
341
|
+
const id = readProjectionAlias(cwd, alias);
|
|
342
|
+
if (id)
|
|
343
|
+
aliases.set(id, alias);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (!(isErrnoException(error) && error.code === "ENOENT"))
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
const rows = [];
|
|
351
|
+
for (const akuma of entries.sort()) {
|
|
352
|
+
if (akuma.startsWith("."))
|
|
353
|
+
continue;
|
|
354
|
+
const akumaDir = path.join(root, akuma);
|
|
269
355
|
try {
|
|
270
|
-
|
|
356
|
+
if (!fs.lstatSync(akumaDir).isDirectory())
|
|
357
|
+
continue;
|
|
271
358
|
}
|
|
272
359
|
catch {
|
|
273
|
-
|
|
360
|
+
continue;
|
|
274
361
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
362
|
+
for (const hex of fs.readdirSync(akumaDir).sort()) {
|
|
363
|
+
const id = `${akuma}/${hex}`;
|
|
364
|
+
const dir = path.join(akumaDir, hex);
|
|
365
|
+
if (!isProjectionId(id))
|
|
366
|
+
continue;
|
|
367
|
+
try {
|
|
368
|
+
if (!fs.lstatSync(dir).isDirectory())
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
rows.push(readProjectionRow(dir, id, aliases.get(id), nowMs));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return rows.length > 0 ? prioritizeProjectionStatusRows(rows, options.showAll === true) : null;
|
|
279
378
|
}
|
package/build/core/status.js
CHANGED
|
@@ -528,7 +528,7 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
528
528
|
const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
|
|
529
529
|
return {
|
|
530
530
|
contracts: filteredContracts,
|
|
531
|
-
projections: readProjectionStatusBoard(cwd, now),
|
|
531
|
+
projections: readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
|
|
532
532
|
queueLength,
|
|
533
533
|
defaultBranch: defaultBranch?.branch,
|
|
534
534
|
defaultHead: defaultBranch?.head,
|