@astrosheep/keiyaku 4.6.12 → 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.
Files changed (62) hide show
  1. package/build/plugins/claude/skills/keiyaku/SKILL.md +3 -0
  2. package/build/plugins/claude/skills/keiyaku-akuma/SKILL.md +20 -0
  3. package/build/plugins/codex/skills/keiyaku/SKILL.md +3 -0
  4. package/build/plugins/codex/skills/keiyaku-akuma/SKILL.md +20 -0
  5. package/build/plugins/opencode/skills/keiyaku/SKILL.md +3 -0
  6. package/build/plugins/opencode/skills/keiyaku-akuma/SKILL.md +20 -0
  7. package/build/skills/keiyaku/SKILL.md +3 -0
  8. package/build/skills/keiyaku-akuma/SKILL.md +20 -0
  9. package/build/src/akuma/akuma-instance.js +7 -5
  10. package/build/src/akuma/akuma-observe.js +1 -0
  11. package/build/src/akuma/akuma-owner.js +3 -2
  12. package/build/src/akuma/akuma-wait.d.ts +1 -2
  13. package/build/src/akuma/akuma-wait.js +1 -4
  14. package/build/src/akuma/akuma.d.ts +9 -4
  15. package/build/src/akuma/akuma.js +208 -35
  16. package/build/src/akuma/body.js +4 -0
  17. package/build/src/akuma/caller.d.ts +2 -0
  18. package/build/src/akuma/caller.js +6 -0
  19. package/build/src/akuma/custody.d.ts +11 -0
  20. package/build/src/akuma/custody.js +102 -0
  21. package/build/src/akuma/lineage-cache.d.ts +39 -0
  22. package/build/src/akuma/lineage-cache.js +162 -0
  23. package/build/src/akuma/nuke.d.ts +1 -3
  24. package/build/src/akuma/nuke.js +11 -102
  25. package/build/src/akuma/providers/pi/index.js +10 -3
  26. package/build/src/akuma/retention.d.ts +44 -0
  27. package/build/src/akuma/retention.js +106 -0
  28. package/build/src/akuma/selection-owner-port.d.ts +1 -1
  29. package/build/src/akuma/selection-owner-port.js +3 -6
  30. package/build/src/akuma/selection-request.d.ts +131 -4
  31. package/build/src/akuma/selection-request.js +25 -15
  32. package/build/src/alias/index.d.ts +11 -0
  33. package/build/src/alias/index.js +20 -1
  34. package/build/src/cli/commands/contract-grammar.d.ts +12 -0
  35. package/build/src/cli/commands/contract-grammar.js +65 -13
  36. package/build/src/cli/commands/contract-help.d.ts +3 -1
  37. package/build/src/cli/commands/contract-help.js +5 -2
  38. package/build/src/cli/commands/contract.js +31 -4
  39. package/build/src/cli/render/akuma-activity.d.ts +1 -1
  40. package/build/src/cli/render/akuma-activity.js +11 -14
  41. package/build/src/cli/render/catalog.d.ts +2 -1
  42. package/build/src/cli/render/catalog.js +8 -3
  43. package/build/src/cli/render/kanshi.js +0 -1
  44. package/build/src/cli/render/terminal.d.ts +0 -1
  45. package/build/src/cli/render/terminal.js +1 -1
  46. package/build/src/cli/square-identity.d.ts +0 -5
  47. package/build/src/cli/square-identity.js +51 -101
  48. package/build/src/index.d.ts +1 -1
  49. package/build/src/library/akuma-retention.d.ts +11 -0
  50. package/build/src/library/akuma-retention.js +131 -0
  51. package/build/src/library/akumas.d.ts +8 -0
  52. package/build/src/library/akumas.js +42 -2
  53. package/build/src/library/selection.js +22 -9
  54. package/build/src/plugin/akuma-signals.d.ts +1 -0
  55. package/build/src/plugin/akuma-signals.js +17 -0
  56. package/build/src/plugin/public.d.ts +7 -0
  57. package/build/src/plugin/runtime.d.ts +1 -0
  58. package/build/src/plugin/runtime.js +85 -13
  59. package/build/src/runtime/proc/windows-launch.exe +0 -0
  60. package/package.json +4 -4
  61. package/skills/keiyaku/SKILL.md +3 -0
  62. package/skills/keiyaku-akuma/SKILL.md +20 -0
@@ -34,3 +34,9 @@ export function decodeAkumaCaller(value) {
34
34
  throw new TypeError(`Invalid Akuma caller: ${parsed.error.message}`);
35
35
  return parsed.data;
36
36
  }
37
+ /** One stable comparison coordinate for stored and queried caller attribution. */
38
+ export function akumaCallerKey(caller) {
39
+ return caller.kind === "akuma"
40
+ ? `akuma\u0000${caller.world}\u0000${caller.akuId}`
41
+ : `external\u0000${caller.integration}\u0000${caller.id}`;
42
+ }
@@ -0,0 +1,11 @@
1
+ import type { KnownAkuma } from "./akuma.js";
2
+ export declare class AkumaResetStopError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function removeKnownRegularFile(path: string): Promise<void>;
6
+ export declare function removeEmptyDirectory(path: string): Promise<void>;
7
+ /**
8
+ * Remove exactly the recognized custody of one Akuma entry. Unknown files and
9
+ * directories stay; an entry whose unknown bytes remain keeps its directory.
10
+ */
11
+ export declare function removeAkumaCustody(entry: KnownAkuma): Promise<void>;
@@ -0,0 +1,102 @@
1
+ import { lstat, readdir, rm, rmdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export class AkumaResetStopError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "AkumaResetStopError";
7
+ }
8
+ }
9
+ export async function removeKnownRegularFile(path) {
10
+ try {
11
+ const value = await lstat(path);
12
+ if (!value.isFile() || value.isSymbolicLink())
13
+ throw new AkumaResetStopError(`Akuma custody is not a regular file: ${path}`);
14
+ await rm(path, { force: true });
15
+ }
16
+ catch (error) {
17
+ if (error.code !== "ENOENT")
18
+ throw error;
19
+ }
20
+ }
21
+ export async function removeEmptyDirectory(path) {
22
+ try {
23
+ await rmdir(path);
24
+ }
25
+ catch (error) {
26
+ if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY")
27
+ throw error;
28
+ }
29
+ }
30
+ function isRequestSequenceName(name) {
31
+ const sequence = Number(name);
32
+ return /^\d+$/u.test(name) && Number.isSafeInteger(sequence) && String(sequence) === name;
33
+ }
34
+ const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
35
+ function isRequestArtifactName(name) {
36
+ const match = /^(.*)\.(?:request|receipt)\.json$/u.exec(name);
37
+ return match !== null && UUID_V4.test(match[1]);
38
+ }
39
+ async function removeKnownRequestFile(path) {
40
+ try {
41
+ const value = await lstat(path);
42
+ if (!value.isFile() || value.isSymbolicLink())
43
+ return;
44
+ await rm(path, { force: true });
45
+ }
46
+ catch (error) {
47
+ if (error.code !== "ENOENT")
48
+ throw error;
49
+ }
50
+ }
51
+ async function removeKnownRequestChannel(path) {
52
+ try {
53
+ const value = await lstat(path);
54
+ if (!value.isDirectory() || value.isSymbolicLink())
55
+ throw new AkumaResetStopError(`Akuma custody is not a directory: ${path}`);
56
+ }
57
+ catch (error) {
58
+ if (error.code === "ENOENT")
59
+ return;
60
+ throw error;
61
+ }
62
+ for (const name of await readdir(path)) {
63
+ if (!isRequestSequenceName(name))
64
+ continue;
65
+ const sequenceDir = join(path, name);
66
+ try {
67
+ const sequence = await lstat(sequenceDir);
68
+ if (!sequence.isDirectory() || sequence.isSymbolicLink())
69
+ continue;
70
+ for (const child of await readdir(sequenceDir)) {
71
+ if (isRequestArtifactName(child))
72
+ await removeKnownRequestFile(join(sequenceDir, child));
73
+ }
74
+ await removeEmptyDirectory(sequenceDir);
75
+ }
76
+ catch (error) {
77
+ if (error.code !== "ENOENT")
78
+ throw error;
79
+ }
80
+ }
81
+ await removeEmptyDirectory(path);
82
+ }
83
+ /**
84
+ * Remove exactly the recognized custody of one Akuma entry. Unknown files and
85
+ * directories stay; an entry whose unknown bytes remain keeps its directory.
86
+ */
87
+ export async function removeAkumaCustody(entry) {
88
+ await removeKnownRequestChannel(entry.paths.requests);
89
+ for (const path of [
90
+ join(entry.paths.directory, "request-entry.json"),
91
+ entry.paths.log,
92
+ `${entry.paths.heart}-wal`,
93
+ `${entry.paths.heart}-shm`,
94
+ `${entry.paths.leash}-wal`,
95
+ `${entry.paths.leash}-shm`,
96
+ entry.paths.leash,
97
+ entry.paths.heart,
98
+ ]) {
99
+ await removeKnownRegularFile(path);
100
+ }
101
+ await removeEmptyDirectory(entry.paths.directory);
102
+ }
@@ -0,0 +1,39 @@
1
+ import { type AkumaCaller } from "./caller.js";
2
+ import { type AkuId, type AkumaPaths } from "./identity.js";
3
+ import type { WorldRoot } from "../world.js";
4
+ /**
5
+ * The World-local lineage cache is a disposable reconstruction of the frozen
6
+ * `Soul.caller` edge for born Akuma. It is never authority: every entry can be
7
+ * rebuilt from Hearts, and an unavailable cache falls back to direct Soul reads.
8
+ */
9
+ export type AkumaLineage = ReadonlyMap<AkuId, AkumaCaller | null>;
10
+ export type LineageMember = Readonly<{
11
+ id: AkuId;
12
+ paths: AkumaPaths;
13
+ }>;
14
+ export declare function akumaLineageCachePath(world: WorldRoot): string;
15
+ /** Authoritative lineage read: every known born Soul, with no cache consultation. */
16
+ export declare function readDirectAkumaLineage(members: readonly LineageMember[]): Promise<AkumaLineage>;
17
+ /**
18
+ * Discard the disposable record when an offered edge disagrees with a born
19
+ * Soul. The next caller-scoped answer comes from direct Soul evidence and
20
+ * rewrites the record, so a wrong edge can never survive as accepted fact.
21
+ */
22
+ export declare function invalidateAkumaLineage(world: WorldRoot): Promise<void>;
23
+ /** Rewrite every edge from authoritative evidence, dropping any stale entry. */
24
+ export declare function rebuildAkumaLineage(world: WorldRoot, edges: AkumaLineage): Promise<void>;
25
+ /**
26
+ * Record one born Soul's immutable caller edge. Best-effort: callers never await
27
+ * this to decide birth, and a failed write is repaired by the next fallback read.
28
+ */
29
+ export declare function recordAkumaLineage(world: WorldRoot, soul: Readonly<{
30
+ id: AkuId;
31
+ caller?: AkumaCaller;
32
+ }>): Promise<void>;
33
+ /**
34
+ * Load the caller edge of every currently known Akuma. Cache hits avoid opening
35
+ * their Hearts; misses and unusable caches fall back to the existing Soul seam.
36
+ * Rows whose Akuma directory no longer exists never join this view, and fresh
37
+ * edges plus that pruning are written back best-effort.
38
+ */
39
+ export declare function readAkumaLineage(world: WorldRoot, members: readonly LineageMember[]): Promise<AkumaLineage>;
@@ -0,0 +1,162 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { lstat, mkdir, rm } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { decodeAkumaCaller } from "./caller.js";
5
+ import { readSoul } from "./heart/index.js";
6
+ import { akumaRunRoot, parseAkuId } from "./identity.js";
7
+ import { boundedMap } from "./read-pool.js";
8
+ const CACHE_FILE = "lineage-cache.sqlite";
9
+ const LINEAGE_SCHEMA = `
10
+ CREATE TABLE IF NOT EXISTS lineage (
11
+ aku_id TEXT PRIMARY KEY,
12
+ caller_json TEXT
13
+ ) STRICT;
14
+ `;
15
+ export function akumaLineageCachePath(world) {
16
+ return join(akumaRunRoot(world), CACHE_FILE);
17
+ }
18
+ function readCachedEdge(row) {
19
+ const value = row;
20
+ if (value === null || typeof value !== "object")
21
+ throw new Error("Akuma lineage cache row is not an object");
22
+ if (typeof value.aku_id !== "string")
23
+ throw new Error("Akuma lineage cache row has no identity");
24
+ const akuId = parseAkuId(value.aku_id).id;
25
+ if (value.caller_json === null)
26
+ return [akuId, null];
27
+ if (typeof value.caller_json !== "string")
28
+ throw new Error("Akuma lineage cache row has no caller evidence");
29
+ const caller = decodeAkumaCaller(JSON.parse(value.caller_json));
30
+ if (caller === undefined)
31
+ throw new Error("Akuma lineage cache row has an empty caller");
32
+ return [akuId, caller];
33
+ }
34
+ /** A readable cache, an empty map for a cache that was never written, or null when unusable. */
35
+ async function readLineageCache(path) {
36
+ try {
37
+ await lstat(path);
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ENOENT")
41
+ return new Map();
42
+ return null;
43
+ }
44
+ let database;
45
+ try {
46
+ // Existing custody is opened without a journal-mode constraint: a writer's
47
+ // WAL side files stay usable, and a busy or unreadable record falls back.
48
+ database = new DatabaseSync(path);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ try {
54
+ database.exec("PRAGMA busy_timeout=0");
55
+ const rows = database.prepare("SELECT aku_id, caller_json FROM lineage").all();
56
+ return new Map(rows.map(readCachedEdge));
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ finally {
62
+ database.close();
63
+ }
64
+ }
65
+ async function writeLineageCache(path, fresh, stale, replace = false) {
66
+ try {
67
+ await mkdir(dirname(path), { recursive: true });
68
+ const database = new DatabaseSync(path);
69
+ try {
70
+ database.exec("PRAGMA busy_timeout=0");
71
+ database.exec("PRAGMA journal_mode=WAL");
72
+ database.exec(LINEAGE_SCHEMA);
73
+ const insert = database.prepare("INSERT OR REPLACE INTO lineage(aku_id, caller_json) VALUES (?, ?)");
74
+ const remove = database.prepare("DELETE FROM lineage WHERE aku_id = ?");
75
+ database.exec("BEGIN IMMEDIATE");
76
+ try {
77
+ if (replace)
78
+ database.exec("DELETE FROM lineage");
79
+ for (const [akuId, caller] of fresh)
80
+ insert.run(akuId, caller === null ? null : JSON.stringify(caller));
81
+ for (const akuId of stale)
82
+ remove.run(akuId);
83
+ database.exec("COMMIT");
84
+ }
85
+ catch (error) {
86
+ database.exec("ROLLBACK");
87
+ throw error;
88
+ }
89
+ }
90
+ finally {
91
+ database.close();
92
+ }
93
+ }
94
+ catch {
95
+ /* The cache is disposable; a failed refresh never changes an Akuma result. */
96
+ }
97
+ }
98
+ async function directEdge(member) {
99
+ try {
100
+ const soul = await readSoul(member.paths);
101
+ return soul === null ? null : [member.id, soul.caller ?? null];
102
+ }
103
+ catch {
104
+ // A hard Heart read failure stays an omitted member, never an empty roster claim.
105
+ return null;
106
+ }
107
+ }
108
+ /** Authoritative lineage read: every known born Soul, with no cache consultation. */
109
+ export async function readDirectAkumaLineage(members) {
110
+ return new Map((await boundedMap(members, directEdge)).filter((edge) => edge !== null));
111
+ }
112
+ /**
113
+ * Discard the disposable record when an offered edge disagrees with a born
114
+ * Soul. The next caller-scoped answer comes from direct Soul evidence and
115
+ * rewrites the record, so a wrong edge can never survive as accepted fact.
116
+ */
117
+ export async function invalidateAkumaLineage(world) {
118
+ const path = akumaLineageCachePath(world);
119
+ for (const file of [path, `${path}-wal`, `${path}-shm`]) {
120
+ try {
121
+ await rm(file, { force: true });
122
+ }
123
+ catch {
124
+ /* Best effort: a still-readable record is checked again by the next query. */
125
+ }
126
+ }
127
+ }
128
+ /** Rewrite every edge from authoritative evidence, dropping any stale entry. */
129
+ export async function rebuildAkumaLineage(world, edges) {
130
+ await writeLineageCache(akumaLineageCachePath(world), [...edges], [], true);
131
+ }
132
+ /**
133
+ * Record one born Soul's immutable caller edge. Best-effort: callers never await
134
+ * this to decide birth, and a failed write is repaired by the next fallback read.
135
+ */
136
+ export async function recordAkumaLineage(world, soul) {
137
+ await writeLineageCache(akumaLineageCachePath(world), [[soul.id, soul.caller ?? null]], []);
138
+ }
139
+ /**
140
+ * Load the caller edge of every currently known Akuma. Cache hits avoid opening
141
+ * their Hearts; misses and unusable caches fall back to the existing Soul seam.
142
+ * Rows whose Akuma directory no longer exists never join this view, and fresh
143
+ * edges plus that pruning are written back best-effort.
144
+ */
145
+ export async function readAkumaLineage(world, members) {
146
+ const path = akumaLineageCachePath(world);
147
+ const cached = await readLineageCache(path);
148
+ if (cached === null)
149
+ return await readDirectAkumaLineage(members);
150
+ const known = new Set(members.map((member) => member.id));
151
+ const stale = [...cached.keys()].filter((id) => !known.has(id));
152
+ const missing = members.filter((member) => !cached.has(member.id));
153
+ const fresh = (await boundedMap(missing, directEdge)).filter((edge) => edge !== null);
154
+ // A vanished directory never contributes an edge even if its prune write fails.
155
+ for (const id of stale)
156
+ cached.delete(id);
157
+ for (const [akuId, caller] of fresh)
158
+ cached.set(akuId, caller);
159
+ if (fresh.length > 0 || stale.length > 0)
160
+ await writeLineageCache(path, fresh, stale);
161
+ return cached;
162
+ }
@@ -1,5 +1,3 @@
1
1
  import type { WorldRoot } from "../world.js";
2
- export declare class AkumaResetStopError extends Error {
3
- constructor(message: string);
4
- }
2
+ export { AkumaResetStopError } from "./custody.js";
5
3
  export declare function stopAkuma(world: WorldRoot): Promise<() => Promise<void>>;
@@ -1,17 +1,14 @@
1
- import { lstat, readdir, rm, rmdir } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
1
+ import { lstat, readdir } from "node:fs/promises";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import { nukeAliases } from "../alias/index.js";
4
4
  import { knownAkuma } from "./akuma.js";
5
+ import { akumaLineageCachePath } from "./lineage-cache.js";
5
6
  import { settleAkumaKill } from "./akuma-owner.js";
6
7
  import { acquireLeash } from "./control.js";
8
+ import { AkumaResetStopError, removeAkumaCustody, removeEmptyDirectory, removeKnownRegularFile } from "./custody.js";
7
9
  import { classifyHeartSchema, readHeart } from "./heart/index.js";
8
10
  import { akumaRunRoot } from "./identity.js";
9
- export class AkumaResetStopError extends Error {
10
- constructor(message) {
11
- super(message);
12
- this.name = "AkumaResetStopError";
13
- }
14
- }
11
+ export { AkumaResetStopError } from "./custody.js";
15
12
  async function hasAkumaCustody(paths) {
16
13
  try {
17
14
  const value = await lstat(paths.heart);
@@ -29,98 +26,10 @@ async function stopRunningAkuma(entry) {
29
26
  return settled.leash;
30
27
  throw new AkumaResetStopError(`Akuma ${entry.id} could not be stopped: ${settled.evidence}`);
31
28
  }
32
- async function removeKnownRegularFile(path) {
33
- try {
34
- const value = await lstat(path);
35
- if (!value.isFile() || value.isSymbolicLink())
36
- throw new AkumaResetStopError(`Akuma custody is not a regular file: ${path}`);
37
- await rm(path, { force: true });
38
- }
39
- catch (error) {
40
- if (error.code !== "ENOENT")
41
- throw error;
42
- }
43
- }
44
- async function removeEmptyDirectory(path) {
45
- try {
46
- await rmdir(path);
47
- }
48
- catch (error) {
49
- if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY")
50
- throw error;
51
- }
52
- }
53
- function isRequestSequenceName(name) {
54
- const sequence = Number(name);
55
- return /^\d+$/u.test(name) && Number.isSafeInteger(sequence) && String(sequence) === name;
56
- }
57
- const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
58
- function isRequestArtifactName(name) {
59
- const match = /^(.*)\.(?:request|receipt)\.json$/u.exec(name);
60
- return match !== null && UUID_V4.test(match[1]);
61
- }
62
- async function removeKnownRequestFile(path) {
63
- try {
64
- const value = await lstat(path);
65
- if (!value.isFile() || value.isSymbolicLink())
66
- return;
67
- await rm(path, { force: true });
68
- }
69
- catch (error) {
70
- if (error.code !== "ENOENT")
71
- throw error;
72
- }
73
- }
74
- async function removeKnownRequestChannel(path) {
75
- try {
76
- const value = await lstat(path);
77
- if (!value.isDirectory() || value.isSymbolicLink())
78
- throw new AkumaResetStopError(`Akuma custody is not a directory: ${path}`);
79
- }
80
- catch (error) {
81
- if (error.code === "ENOENT")
82
- return;
83
- throw error;
84
- }
85
- for (const name of await readdir(path)) {
86
- if (!isRequestSequenceName(name))
87
- continue;
88
- const sequenceDir = join(path, name);
89
- try {
90
- const sequence = await lstat(sequenceDir);
91
- if (!sequence.isDirectory() || sequence.isSymbolicLink())
92
- continue;
93
- for (const child of await readdir(sequenceDir)) {
94
- if (isRequestArtifactName(child))
95
- await removeKnownRequestFile(join(sequenceDir, child));
96
- }
97
- await removeEmptyDirectory(sequenceDir);
98
- }
99
- catch (error) {
100
- if (error.code !== "ENOENT")
101
- throw error;
102
- }
103
- }
104
- await removeEmptyDirectory(path);
105
- }
106
- async function removeAkumaEntry(entry) {
107
- await removeKnownRequestChannel(entry.paths.requests);
108
- for (const path of [
109
- join(entry.paths.directory, "request-entry.json"),
110
- entry.paths.log,
111
- `${entry.paths.heart}-wal`,
112
- `${entry.paths.heart}-shm`,
113
- `${entry.paths.leash}-wal`,
114
- `${entry.paths.leash}-shm`,
115
- entry.paths.leash,
116
- entry.paths.heart,
117
- ]) {
118
- await removeKnownRegularFile(path);
119
- }
120
- await removeEmptyDirectory(entry.paths.directory);
121
- }
122
29
  async function removeEmptyAkumaRunRoot(world) {
123
30
  const runRoot = akumaRunRoot(world);
31
+ const cache = basename(akumaLineageCachePath(world));
32
+ const custody = new Set([".gitignore", cache, `${cache}-wal`, `${cache}-shm`]);
124
33
  let names;
125
34
  try {
126
35
  names = await readdir(runRoot);
@@ -130,10 +39,10 @@ async function removeEmptyAkumaRunRoot(world) {
130
39
  return;
131
40
  throw error;
132
41
  }
133
- if (names.some((name) => name !== ".gitignore"))
42
+ if (names.some((name) => !custody.has(name)))
134
43
  return;
135
- if (names.includes(".gitignore"))
136
- await removeKnownRegularFile(join(runRoot, ".gitignore"));
44
+ for (const name of names)
45
+ await removeKnownRegularFile(join(runRoot, name));
137
46
  await removeEmptyDirectory(runRoot);
138
47
  await removeEmptyDirectory(dirname(runRoot));
139
48
  }
@@ -177,7 +86,7 @@ export async function stopAkuma(world) {
177
86
  try {
178
87
  await nukeAliases(world);
179
88
  for (const entry of entries)
180
- await removeAkumaEntry(entry);
89
+ await removeAkumaCustody(entry);
181
90
  await removeEmptyAkumaRunRoot(world);
182
91
  }
183
92
  finally {
@@ -1,6 +1,6 @@
1
1
  import { abortable } from "../../abort.js";
2
2
  import { akumaExecutionEnvironment } from "../execution-environment.js";
3
- import { AgentEventChannel, createProviderAttempt, launchPrompt, modelEvent, tellRegistry, } from "../../provider.js";
3
+ import { AgentEventChannel, createProviderAttempt, launchPrompt, modelEvent, noteEvent, tellRegistry, } from "../../provider.js";
4
4
  import { piSessionModel, piTerminalFailure, translatePiEvent } from "./events.js";
5
5
  const PI_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
6
6
  const MODEL_PATTERN = /^[^/\s]+\/[^\s]+$/u;
@@ -70,9 +70,13 @@ async function createPiSession(sdk, execution, input) {
70
70
  : { systemPromptOverride: () => input.options.systemPrompt },
71
71
  }),
72
72
  });
73
+ const extensionErrors = services.resourceLoader.getExtensions().errors ?? [];
74
+ for (const failure of extensionErrors)
75
+ console.error(`Pi extension load failed: ${failure.path}\n${failure.error}`);
73
76
  try {
74
77
  input.signal?.throwIfAborted();
75
- return await sdk.createAgentSessionFromServices(piCreateOptions(sdk, input, services));
78
+ const created = await sdk.createAgentSessionFromServices(piCreateOptions(sdk, input, services));
79
+ return { created, extensionErrors };
76
80
  }
77
81
  catch (error) {
78
82
  // Before a session owns the extension runtime, failed selection/setup must
@@ -139,7 +143,7 @@ async function retirePiSession(native) {
139
143
  throw failure;
140
144
  }
141
145
  async function drivePi(sdk, execution, input, signal, custody) {
142
- const created = await createPiSession(sdk, execution, input);
146
+ const { created, extensionErrors } = await createPiSession(sdk, execution, input);
143
147
  const native = created.session;
144
148
  let settleRetired;
145
149
  let failRetired;
@@ -248,6 +252,9 @@ async function drivePi(sdk, execution, input, signal, custody) {
248
252
  await abortable(binding, signal);
249
253
  signal.throwIfAborted();
250
254
  events.emit({ type: "session", coordinate: { sessionFile: native.sessionFile, sessionId: native.sessionId } });
255
+ for (const failure of extensionErrors) {
256
+ events.emit(noteEvent(`Pi extension load failed: ${failure.path}: ${failure.error}`));
257
+ }
251
258
  const sessionModel = piSessionModel(native.model);
252
259
  if (sessionModel !== undefined)
253
260
  events.emit(modelEvent(sessionModel));
@@ -0,0 +1,44 @@
1
+ import type { KnownAkuma } from "./akuma.js";
2
+ /** The one default age after which an inactive Akuma entry may be retired. */
3
+ export declare const AKUMA_RETENTION_MAX_IDLE_MS: number;
4
+ /** The retention owner's one clock reading; the sweep itself never reads ambient time. */
5
+ export declare function retentionNow(): number;
6
+ export type AkumaRetentionReceipt = "retired" | "retained" | "busy";
7
+ /** A cross-authority custody decision that may hold its locks across the effect. */
8
+ export type AkumaRetirementCustody = (retire: () => Promise<boolean>) => Promise<AkumaRetentionReceipt>;
9
+ /**
10
+ * The later durable life or activity instant, or null when either present
11
+ * timestamp cannot be read. A missing timestamp contributes nothing; a
12
+ * malformed one means this observation cannot prove age.
13
+ */
14
+ export declare function latestRetentionActivity(lifeAtValue: string | null, lastActivityAt: string | null): number | null;
15
+ /**
16
+ * A cheap metadata bound, never semantic proof. Only a Heart whose last
17
+ * filesystem write is already older than the retention age reaches a Heart
18
+ * open; unreadable metadata stays conservative and skips the candidate.
19
+ */
20
+ export declare function retentionCandidateBound(paths: KnownAkuma["paths"], now: number): Promise<boolean>;
21
+ /**
22
+ * One born, readable, stopped entry whose durable activity is strictly older
23
+ * than the default retention age. Unborn, stillborn, running, pending-input,
24
+ * malformed, and missing-age entries never become candidates.
25
+ */
26
+ export declare function readRetentionCandidate(entry: KnownAkuma, now: number): Promise<KnownAkuma | null>;
27
+ /**
28
+ * Revalidate one candidate under exclusive stopped custody and remove its
29
+ * recognized custody. The caller's custody action runs while the leash is
30
+ * held and may hold Alias or Dispatch publication custody across the `retire`
31
+ * action, so a concurrent wake, alias move, or new Dispatch cannot interleave
32
+ * between the final judgment and removal. `retire` itself re-reads Heart as the
33
+ * last step before deleting and refuses when the entry is no longer eligible.
34
+ * Tell admission has no shared custody here: a pending Tell observed by that
35
+ * final read is preserved, but an admission immediately after it is a
36
+ * documented best-effort boundary, not an atomic guarantee. A busy leash,
37
+ * changed Heart, admitted pending input, or refused judgment leaves the entry
38
+ * intact.
39
+ */
40
+ export declare function retireInactiveAkuma(input: Readonly<{
41
+ entry: KnownAkuma;
42
+ now: number;
43
+ retireUnderCustody: AkumaRetirementCustody;
44
+ }>): Promise<AkumaRetentionReceipt>;
@@ -0,0 +1,106 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { rosterListRow } from "./akuma-observe.js";
3
+ import { removeAkumaCustody } from "./custody.js";
4
+ import { HeldAkumaLeash, life, lifeAt, readHeart } from "./heart/index.js";
5
+ /** The one default age after which an inactive Akuma entry may be retired. */
6
+ export const AKUMA_RETENTION_MAX_IDLE_MS = 15 * 24 * 60 * 60 * 1_000;
7
+ /** The retention owner's one clock reading; the sweep itself never reads ambient time. */
8
+ export function retentionNow() {
9
+ return new Date().getTime();
10
+ }
11
+ /**
12
+ * The later durable life or activity instant, or null when either present
13
+ * timestamp cannot be read. A missing timestamp contributes nothing; a
14
+ * malformed one means this observation cannot prove age.
15
+ */
16
+ export function latestRetentionActivity(lifeAtValue, lastActivityAt) {
17
+ const instants = [lifeAtValue, lastActivityAt].flatMap((value) => (value === null ? [] : [Date.parse(value)]));
18
+ if (instants.some((instant) => !Number.isFinite(instant)))
19
+ return null;
20
+ return instants.length === 0 ? null : Math.max(...instants);
21
+ }
22
+ /** The strict age cutoff shared by candidate reading and final revalidation. */
23
+ function isStale(now, activityAt) {
24
+ return activityAt !== null && now - activityAt > AKUMA_RETENTION_MAX_IDLE_MS;
25
+ }
26
+ /**
27
+ * A cheap metadata bound, never semantic proof. Only a Heart whose last
28
+ * filesystem write is already older than the retention age reaches a Heart
29
+ * open; unreadable metadata stays conservative and skips the candidate.
30
+ */
31
+ export async function retentionCandidateBound(paths, now) {
32
+ const read = async (path) => {
33
+ try {
34
+ const value = await lstat(path);
35
+ return value.isFile() ? value.mtimeMs : Number.POSITIVE_INFINITY;
36
+ }
37
+ catch (error) {
38
+ return error.code === "ENOENT" ? 0 : Number.POSITIVE_INFINITY;
39
+ }
40
+ };
41
+ const bound = Math.max(await read(paths.heart), await read(`${paths.heart}-wal`));
42
+ if (!Number.isFinite(bound))
43
+ return false;
44
+ return bound < now - AKUMA_RETENTION_MAX_IDLE_MS;
45
+ }
46
+ /**
47
+ * One born, readable, stopped entry whose durable activity is strictly older
48
+ * than the default retention age. Unborn, stillborn, running, pending-input,
49
+ * malformed, and missing-age entries never become candidates.
50
+ */
51
+ export async function readRetentionCandidate(entry, now) {
52
+ const row = await rosterListRow(entry.paths, entry.id);
53
+ if (!("lifeAt" in row) || row.life === "running" || row.pending.length > 0)
54
+ return null;
55
+ return isStale(now, latestRetentionActivity(row.lifeAt, row.lastActivityAt)) ? entry : null;
56
+ }
57
+ function currentStoppedActivity(snapshot) {
58
+ const soul = snapshot.soul;
59
+ if (soul === null)
60
+ return null;
61
+ if (snapshot.latestBody?.end === undefined)
62
+ return null;
63
+ if (snapshot.pending.length > 0)
64
+ return null;
65
+ const currentLife = life({ leash: "free", body: snapshot.latestBody, kill: snapshot.latestKill });
66
+ if (currentLife === "running")
67
+ return null;
68
+ return latestRetentionActivity(lifeAt(currentLife, snapshot.latestBody, snapshot.latestKill, soul.createdAt), snapshot.lastActivityAt);
69
+ }
70
+ /**
71
+ * Revalidate one candidate under exclusive stopped custody and remove its
72
+ * recognized custody. The caller's custody action runs while the leash is
73
+ * held and may hold Alias or Dispatch publication custody across the `retire`
74
+ * action, so a concurrent wake, alias move, or new Dispatch cannot interleave
75
+ * between the final judgment and removal. `retire` itself re-reads Heart as the
76
+ * last step before deleting and refuses when the entry is no longer eligible.
77
+ * Tell admission has no shared custody here: a pending Tell observed by that
78
+ * final read is preserved, but an admission immediately after it is a
79
+ * documented best-effort boundary, not an atomic guarantee. A busy leash,
80
+ * changed Heart, admitted pending input, or refused judgment leaves the entry
81
+ * intact.
82
+ */
83
+ export async function retireInactiveAkuma(input) {
84
+ const leash = await HeldAkumaLeash.try(input.entry.paths);
85
+ if (leash === null)
86
+ return "busy";
87
+ try {
88
+ const eligible = async () => {
89
+ const snapshot = await readHeart(input.entry.paths);
90
+ if (snapshot.soul === null || snapshot.soul.id !== input.entry.id)
91
+ return false;
92
+ return isStale(input.now, currentStoppedActivity(snapshot));
93
+ };
94
+ if (!(await eligible()))
95
+ return "retained";
96
+ return await input.retireUnderCustody(async () => {
97
+ if (!(await eligible()))
98
+ return false;
99
+ await removeAkumaCustody(input.entry);
100
+ return true;
101
+ });
102
+ }
103
+ finally {
104
+ leash.release();
105
+ }
106
+ }