@llblab/pi-kit 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
- package/node_modules/@llblab/pi-state-flow/README.md +102 -48
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
- package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
- package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
- package/node_modules/@llblab/pi-state-flow/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +13 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +72 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +27 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +0 -10
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +18 -41
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +126 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +46 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +8 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
|
@@ -1,437 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, parse, resolve, sep } from "node:path";
|
|
4
|
-
/** Missing policy on an existing runtime preserves 0.4 synchronous behavior. */
|
|
5
|
-
export function resolveRemotePublicationPolicy(value, options) {
|
|
6
|
-
if (value === undefined) {
|
|
7
|
-
return options.legacyRuntime
|
|
8
|
-
? { mode: "transition", migratedLegacyDefault: true }
|
|
9
|
-
: { mode: "turn-end", migratedLegacyDefault: false };
|
|
10
|
-
}
|
|
11
|
-
if (value !== "off" && value !== "turn-end" && value !== "transition") {
|
|
12
|
-
throw new Error("State Flow remote publication mode must be off, turn-end, or transition");
|
|
13
|
-
}
|
|
14
|
-
return { mode: value, migratedLegacyDefault: false };
|
|
15
|
-
}
|
|
16
|
-
export function parseRemotePublicationPolicyDocument(value, options) {
|
|
17
|
-
if (value === undefined)
|
|
18
|
-
return resolveRemotePublicationPolicy(undefined, options);
|
|
19
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
20
|
-
throw new Error("Invalid remote publication policy document");
|
|
21
|
-
const document = value;
|
|
22
|
-
if (Object.keys(document).sort().join(",") !== "mode,version" || document.version !== 1) {
|
|
23
|
-
throw new Error("Invalid remote publication policy document");
|
|
24
|
-
}
|
|
25
|
-
return resolveRemotePublicationPolicy(document.mode, { legacyRuntime: false });
|
|
26
|
-
}
|
|
27
|
-
export function serializeRemotePublicationPolicyDocument(policy) {
|
|
28
|
-
return { version: 1, mode: policy.mode };
|
|
29
|
-
}
|
|
30
|
-
export function remotePublicationDestinationKey(destination) {
|
|
31
|
-
if (!destination.remote.trim() || destination.remote !== destination.remote.trim()
|
|
32
|
-
|| !destination.ref.trim() || destination.ref !== destination.ref.trim()) {
|
|
33
|
-
throw new Error("Remote publication destination requires non-empty trimmed remote and ref");
|
|
34
|
-
}
|
|
35
|
-
return JSON.stringify([resolve(destination.gitCommonDir), destination.remote, destination.ref]);
|
|
36
|
-
}
|
|
37
|
-
function isCommit(value) {
|
|
38
|
-
return typeof value === "string" && /^[0-9a-f]{40,64}$/.test(value);
|
|
39
|
-
}
|
|
40
|
-
export function createPublicationQueue(destination, target) {
|
|
41
|
-
if (!isCommit(target))
|
|
42
|
-
throw new Error("Publication queue target must be an exact commit");
|
|
43
|
-
remotePublicationDestinationKey(destination);
|
|
44
|
-
return { version: 1, destination: structuredClone(destination), target, status: "pending", attempt: 0 };
|
|
45
|
-
}
|
|
46
|
-
export function coalescePublicationTarget(state, destination, target, isAncestor, observer) {
|
|
47
|
-
validatePublicationQueue(state);
|
|
48
|
-
if (!isCommit(target))
|
|
49
|
-
throw new Error("Publication queue target must be an exact commit");
|
|
50
|
-
if (remotePublicationDestinationKey(state.destination) !== remotePublicationDestinationKey(destination))
|
|
51
|
-
throw new Error("Publication queue destination changed");
|
|
52
|
-
if (target === state.target || isAncestor(target, state.target))
|
|
53
|
-
return structuredClone(state);
|
|
54
|
-
const previous = structuredClone(state);
|
|
55
|
-
if (!isAncestor(state.target, target)) {
|
|
56
|
-
// A reset or re-initialized journal rewrites the lineage. The queued commit can never
|
|
57
|
-
// fast-forward the remote again, so retarget the live lineage instead of wedging every
|
|
58
|
-
// later turn-end; the retired commit stays in the local Git object store.
|
|
59
|
-
observer?.onDivergedLineage?.(previous);
|
|
60
|
-
const { confirmed: _confirmed, error: _error, ...live } = previous;
|
|
61
|
-
return { ...live, target, status: "pending", attempt: 0 };
|
|
62
|
-
}
|
|
63
|
-
return { ...previous, target, status: "pending", attempt: 0, error: undefined };
|
|
64
|
-
}
|
|
65
|
-
export function parsePublicationQueue(content) {
|
|
66
|
-
let value;
|
|
67
|
-
try {
|
|
68
|
-
value = JSON.parse(content);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
throw new Error("Invalid publication queue JSON");
|
|
72
|
-
}
|
|
73
|
-
validatePublicationQueue(value);
|
|
74
|
-
return structuredClone(value);
|
|
75
|
-
}
|
|
76
|
-
export function serializePublicationQueue(state) {
|
|
77
|
-
validatePublicationQueue(state);
|
|
78
|
-
return `${JSON.stringify(state)}\n`;
|
|
79
|
-
}
|
|
80
|
-
export function beginPublicationAttempt(state) {
|
|
81
|
-
validatePublicationQueue(state);
|
|
82
|
-
const { error: _error, ...current } = structuredClone(state);
|
|
83
|
-
return { ...current, status: "pushing", attempt: state.attempt + 1 };
|
|
84
|
-
}
|
|
85
|
-
export function failPublicationAttempt(state, error) {
|
|
86
|
-
validatePublicationQueue(state);
|
|
87
|
-
if (state.status !== "pushing" || !error.trim())
|
|
88
|
-
throw new Error("Only an active publication attempt can fail with a bounded error");
|
|
89
|
-
return { ...structuredClone(state), status: "failed", error: error.trim().slice(0, 1000) };
|
|
90
|
-
}
|
|
91
|
-
export function confirmPublicationTarget(state, pushed, isAncestor) {
|
|
92
|
-
validatePublicationQueue(state);
|
|
93
|
-
if (!isCommit(pushed))
|
|
94
|
-
throw new Error("Confirmed publication target must be an exact commit");
|
|
95
|
-
if (pushed === state.target)
|
|
96
|
-
return undefined;
|
|
97
|
-
if (!isAncestor(pushed, state.target))
|
|
98
|
-
throw new Error("Publication confirmation does not cover the queued lineage");
|
|
99
|
-
return { ...structuredClone(state), confirmed: pushed, status: "pending", error: undefined };
|
|
100
|
-
}
|
|
101
|
-
/** A recovered pushing state is unconfirmed and safely retryable after restart. */
|
|
102
|
-
export function recoverPublicationQueue(state) {
|
|
103
|
-
validatePublicationQueue(state);
|
|
104
|
-
return state.status === "pushing"
|
|
105
|
-
? { ...structuredClone(state), status: "pending", error: "previous publication attempt ended without confirmation" }
|
|
106
|
-
: structuredClone(state);
|
|
107
|
-
}
|
|
108
|
-
export function validatePublicationQueue(value) {
|
|
109
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
110
|
-
throw new Error("Invalid publication queue document");
|
|
111
|
-
const v = value;
|
|
112
|
-
if (v.version !== 1 || !isCommit(v.target) || !Number.isSafeInteger(v.attempt) || v.attempt < 0
|
|
113
|
-
|| (v.status !== "pending" && v.status !== "pushing" && v.status !== "failed")
|
|
114
|
-
|| typeof v.destination !== "object" || v.destination === null)
|
|
115
|
-
throw new Error("Invalid publication queue document");
|
|
116
|
-
remotePublicationDestinationKey(v.destination);
|
|
117
|
-
if (v.confirmed !== undefined && !isCommit(v.confirmed))
|
|
118
|
-
throw new Error("Invalid publication queue document");
|
|
119
|
-
if (v.error !== undefined && (typeof v.error !== "string" || !v.error.trim()))
|
|
120
|
-
throw new Error("Invalid publication queue document");
|
|
121
|
-
const allowed = new Set(["version", "destination", "target", "confirmed", "status", "attempt", "error"]);
|
|
122
|
-
if (Object.keys(v).some((key) => !allowed.has(key)))
|
|
123
|
-
throw new Error("Invalid publication queue document");
|
|
124
|
-
}
|
|
125
|
-
export function publicationQueuePath(destination) {
|
|
126
|
-
const key = remotePublicationDestinationKey(destination);
|
|
127
|
-
const name = createHash("sha256").update(key).digest("hex");
|
|
128
|
-
return resolve(destination.gitCommonDir, "state-flow-publication", `${name}.json`);
|
|
129
|
-
}
|
|
130
|
-
function assertNoSymlinkAncestors(path) {
|
|
131
|
-
const absolute = resolve(path);
|
|
132
|
-
const root = parse(absolute).root;
|
|
133
|
-
let current = root;
|
|
134
|
-
for (const segment of absolute.slice(root.length).split(sep).filter(Boolean)) {
|
|
135
|
-
current = resolve(current, segment);
|
|
136
|
-
const stat = lstatSync(current, { throwIfNoEntry: false });
|
|
137
|
-
if (!stat)
|
|
138
|
-
break;
|
|
139
|
-
if (stat.isSymbolicLink())
|
|
140
|
-
throw new Error("Publication queue path cannot traverse symlink ancestors");
|
|
141
|
-
if (!stat.isDirectory())
|
|
142
|
-
throw new Error("Publication queue ancestor must be a directory");
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function readWorkerLease(path) {
|
|
146
|
-
assertNoSymlinkAncestors(dirname(path));
|
|
147
|
-
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
148
|
-
if (!stat)
|
|
149
|
-
return undefined;
|
|
150
|
-
if (!stat.isFile())
|
|
151
|
-
throw new Error("Publication worker lease path must be a regular file");
|
|
152
|
-
let descriptor;
|
|
153
|
-
try {
|
|
154
|
-
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
if (error.code === "ENOENT")
|
|
158
|
-
return undefined; // A live owner may have released it.
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
if (!fstatSync(descriptor).isFile())
|
|
163
|
-
throw new Error("Publication worker lease path must be a regular file");
|
|
164
|
-
const source = readFileSync(descriptor, "utf8");
|
|
165
|
-
let value;
|
|
166
|
-
try {
|
|
167
|
-
value = JSON.parse(source);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
throw new Error("Publication worker lease is malformed");
|
|
171
|
-
}
|
|
172
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
173
|
-
throw new Error("Publication worker lease is malformed");
|
|
174
|
-
const record = value;
|
|
175
|
-
if (Object.keys(record).sort().join(",") !== "pid,startedAt,token,version" || record.version !== 1
|
|
176
|
-
|| !Number.isSafeInteger(record.pid) || record.pid <= 0 || record.pid > 2147483647
|
|
177
|
-
|| typeof record.token !== "string" || !record.token.trim() || record.token !== record.token.trim()
|
|
178
|
-
|| typeof record.startedAt !== "string" || !Number.isFinite(Date.parse(record.startedAt))) {
|
|
179
|
-
throw new Error("Publication worker lease is malformed");
|
|
180
|
-
}
|
|
181
|
-
return record;
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
closeSync(descriptor);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
function processAlive(pid) {
|
|
188
|
-
try {
|
|
189
|
-
process.kill(pid, 0);
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
return error.code !== "ESRCH";
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
export function acquirePublicationWorkerLease(queuePath) {
|
|
197
|
-
const path = `${resolve(queuePath)}.worker.lock`;
|
|
198
|
-
assertNoSymlinkAncestors(dirname(path));
|
|
199
|
-
const token = randomUUID();
|
|
200
|
-
const document = `${JSON.stringify({ version: 1, pid: process.pid, token, startedAt: new Date().toISOString() })}\n`;
|
|
201
|
-
const create = () => {
|
|
202
|
-
try {
|
|
203
|
-
writeFileSync(path, document, { flag: "wx", mode: 0o600 });
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
if (error.code === "EEXIST")
|
|
207
|
-
return undefined;
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
210
|
-
return { path, token, release() {
|
|
211
|
-
let current;
|
|
212
|
-
try {
|
|
213
|
-
current = readWorkerLease(path);
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
// A live owner cannot be reclaimed, so release need not wait for queue writers.
|
|
219
|
-
if (current?.pid === process.pid && current.token === token)
|
|
220
|
-
rmSync(path, { force: true });
|
|
221
|
-
} };
|
|
222
|
-
};
|
|
223
|
-
const lease = create();
|
|
224
|
-
if (lease)
|
|
225
|
-
return lease;
|
|
226
|
-
const current = readWorkerLease(path);
|
|
227
|
-
if (!current)
|
|
228
|
-
return create();
|
|
229
|
-
if (processAlive(current.pid))
|
|
230
|
-
return undefined;
|
|
231
|
-
// Only reclamation needs the existing queue writer gate; exclusive creation protects fresh claims.
|
|
232
|
-
const gate = `${resolve(queuePath)}.lock`;
|
|
233
|
-
assertNoSymlinkAncestors(dirname(gate));
|
|
234
|
-
let descriptor;
|
|
235
|
-
try {
|
|
236
|
-
descriptor = openSync(gate, "wx", 0o600);
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
if (error.code === "EEXIST")
|
|
240
|
-
return undefined;
|
|
241
|
-
throw error;
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
const owner = readWorkerLease(path);
|
|
245
|
-
if (owner && processAlive(owner.pid))
|
|
246
|
-
return undefined;
|
|
247
|
-
if (owner)
|
|
248
|
-
rmSync(path, { force: true });
|
|
249
|
-
return create(); // A fresh claimant may win the gap; never remove its replacement.
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
closeSync(descriptor);
|
|
253
|
-
rmSync(gate, { force: true });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
export function loadPublicationQueue(path) {
|
|
257
|
-
const target = resolve(path);
|
|
258
|
-
assertNoSymlinkAncestors(dirname(target));
|
|
259
|
-
const stat = lstatSync(target, { throwIfNoEntry: false });
|
|
260
|
-
if (!stat)
|
|
261
|
-
return undefined;
|
|
262
|
-
if (!stat.isFile())
|
|
263
|
-
throw new Error("Publication queue path must be a regular file");
|
|
264
|
-
return parsePublicationQueue(readFileSync(target, "utf8"));
|
|
265
|
-
}
|
|
266
|
-
export function removePublicationQueue(path, expected) {
|
|
267
|
-
const target = resolve(path);
|
|
268
|
-
assertNoSymlinkAncestors(dirname(target));
|
|
269
|
-
const lock = `${target}.lock`;
|
|
270
|
-
let lockFd;
|
|
271
|
-
try {
|
|
272
|
-
lockFd = openSync(lock, "wx", 0o600);
|
|
273
|
-
const current = loadPublicationQueue(target);
|
|
274
|
-
if (!current || serializePublicationQueue(current) !== serializePublicationQueue(expected)) {
|
|
275
|
-
throw new Error("Publication queue compare-and-swap conflict");
|
|
276
|
-
}
|
|
277
|
-
rmSync(target);
|
|
278
|
-
}
|
|
279
|
-
finally {
|
|
280
|
-
if (lockFd !== undefined) {
|
|
281
|
-
closeSync(lockFd);
|
|
282
|
-
rmSync(lock, { force: true });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
export function savePublicationQueue(path, state, expected) {
|
|
287
|
-
const target = resolve(path);
|
|
288
|
-
assertNoSymlinkAncestors(dirname(target));
|
|
289
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
290
|
-
const lock = `${target}.lock`;
|
|
291
|
-
let lockFd;
|
|
292
|
-
try {
|
|
293
|
-
lockFd = openSync(lock, "wx", 0o600);
|
|
294
|
-
const existing = lstatSync(target, { throwIfNoEntry: false });
|
|
295
|
-
if (existing && !existing.isFile())
|
|
296
|
-
throw new Error("Publication queue path must be a regular file");
|
|
297
|
-
const previous = existing ? parsePublicationQueue(readFileSync(target, "utf8")) : undefined;
|
|
298
|
-
if (expected !== undefined && (previous === undefined
|
|
299
|
-
|| serializePublicationQueue(previous) !== serializePublicationQueue(expected))) {
|
|
300
|
-
throw new Error("Publication queue compare-and-swap conflict");
|
|
301
|
-
}
|
|
302
|
-
if (expected === undefined && previous !== undefined)
|
|
303
|
-
throw new Error("Publication queue compare-and-swap requires the current receipt");
|
|
304
|
-
const temporary = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
305
|
-
try {
|
|
306
|
-
writeFileSync(temporary, serializePublicationQueue(state), { flag: "wx", mode: 0o600 });
|
|
307
|
-
renameSync(temporary, target);
|
|
308
|
-
}
|
|
309
|
-
finally {
|
|
310
|
-
rmSync(temporary, { force: true });
|
|
311
|
-
}
|
|
312
|
-
return { ...(previous === undefined ? {} : { previous }), current: structuredClone(state) };
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
if (lockFd !== undefined) {
|
|
316
|
-
closeSync(lockFd);
|
|
317
|
-
rmSync(lock, { force: true });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
/** Execute one immutable target attempt; persistence/CAS remains the caller's responsibility. */
|
|
322
|
-
export async function runPublicationWorker(state, push, current, isAncestor) {
|
|
323
|
-
const attempted = beginPublicationAttempt(state);
|
|
324
|
-
try {
|
|
325
|
-
await push(Object.freeze(structuredClone(attempted)));
|
|
326
|
-
const next = confirmPublicationTarget(current(), attempted.target, isAncestor);
|
|
327
|
-
return { attempted, ...(next === undefined ? {} : { next }) };
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
return {
|
|
331
|
-
attempted,
|
|
332
|
-
next: failPublicationAttempt(attempted, error instanceof Error ? error.message : String(error)),
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/** Own durable queue coalescing, worker leases, retries, generation fencing, and bounded shutdown. */
|
|
337
|
-
export class PublicationWorkerController {
|
|
338
|
-
active = new Map();
|
|
339
|
-
ports;
|
|
340
|
-
stopped = false;
|
|
341
|
-
shutdownPromise;
|
|
342
|
-
constructor(ports) {
|
|
343
|
-
this.ports = ports;
|
|
344
|
-
}
|
|
345
|
-
enqueue(target) {
|
|
346
|
-
const destination = this.ports.resolveDestination();
|
|
347
|
-
if (!destination)
|
|
348
|
-
return;
|
|
349
|
-
const path = publicationQueuePath(destination);
|
|
350
|
-
const previous = loadPublicationQueue(path);
|
|
351
|
-
const next = previous
|
|
352
|
-
? coalescePublicationTarget(previous, destination, target, this.ports.isAncestor, {
|
|
353
|
-
onDivergedLineage: (dropped) => this.ports.onDiverged(dropped, target),
|
|
354
|
-
})
|
|
355
|
-
: createPublicationQueue(destination, target);
|
|
356
|
-
savePublicationQueue(path, next, previous);
|
|
357
|
-
}
|
|
358
|
-
launch() {
|
|
359
|
-
if (this.stopped)
|
|
360
|
-
return;
|
|
361
|
-
let destination;
|
|
362
|
-
try {
|
|
363
|
-
destination = this.ports.resolveDestination();
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
if (error instanceof Error && /ENOENT/.test(error.message))
|
|
367
|
-
return;
|
|
368
|
-
throw error;
|
|
369
|
-
}
|
|
370
|
-
if (!destination)
|
|
371
|
-
return;
|
|
372
|
-
const path = publicationQueuePath(destination);
|
|
373
|
-
if (this.active.has(path))
|
|
374
|
-
return;
|
|
375
|
-
let queued;
|
|
376
|
-
let lease;
|
|
377
|
-
try {
|
|
378
|
-
queued = loadPublicationQueue(path);
|
|
379
|
-
if (!queued)
|
|
380
|
-
return;
|
|
381
|
-
lease = acquirePublicationWorkerLease(path);
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (!lease)
|
|
387
|
-
return;
|
|
388
|
-
const controller = new AbortController();
|
|
389
|
-
const done = runPublicationWorker(queued, ({ target }) => this.ports.push(destination, target, controller.signal), () => loadPublicationQueue(path) ?? queued, this.ports.isAncestor).then((result) => {
|
|
390
|
-
if (this.stopped)
|
|
391
|
-
return;
|
|
392
|
-
const current = loadPublicationQueue(path);
|
|
393
|
-
if (!current)
|
|
394
|
-
return;
|
|
395
|
-
if (result.next === undefined) {
|
|
396
|
-
if (current.target === result.attempted.target)
|
|
397
|
-
removePublicationQueue(path, current);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (current.target === result.attempted.target || result.next.target === current.target)
|
|
401
|
-
savePublicationQueue(path, result.next, current);
|
|
402
|
-
}).catch(() => {
|
|
403
|
-
// Queue/CAS truth remains durable; status and a later activation expose retry.
|
|
404
|
-
}).finally(() => {
|
|
405
|
-
this.active.delete(path);
|
|
406
|
-
try {
|
|
407
|
-
lease.release();
|
|
408
|
-
if (!this.stopped && loadPublicationQueue(path)?.status === "pending")
|
|
409
|
-
this.launch();
|
|
410
|
-
}
|
|
411
|
-
catch {
|
|
412
|
-
// Failed lease cleanup or malformed persistence stays inert until retry or repair.
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
this.active.set(path, { controller, done });
|
|
416
|
-
}
|
|
417
|
-
shutdown(waitMs) {
|
|
418
|
-
this.stopped = true;
|
|
419
|
-
return this.shutdownPromise ??= (async () => {
|
|
420
|
-
const workers = [...this.active.values()];
|
|
421
|
-
for (const { controller } of workers)
|
|
422
|
-
controller.abort();
|
|
423
|
-
if (workers.length === 0)
|
|
424
|
-
return true;
|
|
425
|
-
let timeout;
|
|
426
|
-
try {
|
|
427
|
-
return await Promise.race([
|
|
428
|
-
Promise.all(workers.map(({ done }) => done)).then(() => true),
|
|
429
|
-
new Promise((resolve) => { timeout = setTimeout(() => resolve(false), waitMs); }),
|
|
430
|
-
]);
|
|
431
|
-
}
|
|
432
|
-
finally {
|
|
433
|
-
clearTimeout(timeout);
|
|
434
|
-
}
|
|
435
|
-
})();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { hashArtifactSource, type ArtifactSourceIdentity } from "./artifact.ts";
|
|
5
|
-
|
|
6
|
-
/** Opaque source metadata discovered without decoding or retaining Markdown bodies. */
|
|
7
|
-
export interface ArtifactSourceCandidate extends ArtifactSourceIdentity {
|
|
8
|
-
/** Raw byte count used as a conservative maintenance token-budget ceiling. */
|
|
9
|
-
bytes: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface GlobalMarkdownDiscoveryResult {
|
|
13
|
-
sources: ArtifactSourceCandidate[];
|
|
14
|
-
removed: string[];
|
|
15
|
-
unavailable?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getKnowledgeRoot(agentDir = getAgentDir()): string {
|
|
19
|
-
return resolve(agentDir, "knowledge");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function errorCode(error: unknown): string | undefined {
|
|
23
|
-
return error instanceof Error && "code" in error
|
|
24
|
-
? (error as NodeJS.ErrnoException).code
|
|
25
|
-
: undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isMissing(error: unknown): boolean {
|
|
29
|
-
return errorCode(error) === "ENOENT";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isInside(root: string, path: string): boolean {
|
|
33
|
-
const child = relative(root, path);
|
|
34
|
-
return child === "" || (!isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function canonicalExistingPath(path: string): string | undefined {
|
|
38
|
-
try {
|
|
39
|
-
return realpathSync.native(path);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
if (isMissing(error)) return undefined;
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function readRegularFile(path: string): Buffer | undefined {
|
|
47
|
-
let descriptor: number | undefined;
|
|
48
|
-
try {
|
|
49
|
-
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
50
|
-
if (!fstatSync(descriptor).isFile()) return undefined;
|
|
51
|
-
return readFileSync(descriptor);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (isMissing(error) || errorCode(error) === "ELOOP") return undefined;
|
|
54
|
-
throw error;
|
|
55
|
-
} finally {
|
|
56
|
-
if (descriptor !== undefined) closeSync(descriptor);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Discover regular `*.md` files beneath a Knowledge root without following directory or file symlinks. */
|
|
61
|
-
export function discoverGlobalMarkdownSources(knowledgeRoot = getKnowledgeRoot()): ArtifactSourceCandidate[] {
|
|
62
|
-
const configuredRoot = resolve(knowledgeRoot);
|
|
63
|
-
const root = canonicalExistingPath(configuredRoot);
|
|
64
|
-
if (root === undefined) return [];
|
|
65
|
-
if (!statSync(root).isDirectory()) throw new Error(`Knowledge root is not a directory: ${configuredRoot}`);
|
|
66
|
-
|
|
67
|
-
const sources: ArtifactSourceCandidate[] = [];
|
|
68
|
-
const visit = (directory: string): void => {
|
|
69
|
-
const entries = readdirSync(directory, { withFileTypes: true })
|
|
70
|
-
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
71
|
-
for (const entry of entries) {
|
|
72
|
-
const candidate = join(directory, entry.name);
|
|
73
|
-
let metadata;
|
|
74
|
-
try {
|
|
75
|
-
metadata = lstatSync(candidate);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (isMissing(error)) continue;
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
if (metadata.isSymbolicLink()) continue;
|
|
81
|
-
|
|
82
|
-
const canonical = canonicalExistingPath(candidate);
|
|
83
|
-
if (canonical === undefined || !isInside(root, canonical)) continue;
|
|
84
|
-
if (metadata.isDirectory()) {
|
|
85
|
-
visit(canonical);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (!metadata.isFile() || !entry.name.endsWith(".md")) continue;
|
|
89
|
-
|
|
90
|
-
const body = readRegularFile(canonical);
|
|
91
|
-
if (body === undefined) continue;
|
|
92
|
-
sources.push({ path: canonical, hash: hashArtifactSource(body), bytes: body.byteLength });
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
visit(root);
|
|
97
|
-
return sources.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Prove absence inside the current source root without following excluded symlink components. */
|
|
101
|
-
function missingOwnedMarkdown(root: string, path: string): boolean {
|
|
102
|
-
if (!isAbsolute(path) || path !== resolve(path) || path === root || !isInside(root, path) || !path.endsWith(".md")) return false;
|
|
103
|
-
let candidate = root;
|
|
104
|
-
for (const segment of relative(root, path).split(sep)) {
|
|
105
|
-
candidate = join(candidate, segment);
|
|
106
|
-
const metadata = lstatSync(candidate, { throwIfNoEntry: false });
|
|
107
|
-
if (metadata === undefined) return true;
|
|
108
|
-
if (metadata.isSymbolicLink() || !metadata.isDirectory()) return false;
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Source-owner discovery; retained paths let restart/status re-observe removals until publication. */
|
|
114
|
-
export class GlobalMarkdownDiscovery {
|
|
115
|
-
readonly knowledgeRoot: string;
|
|
116
|
-
#knownPaths = new Set<string>();
|
|
117
|
-
|
|
118
|
-
constructor(knowledgeRoot = getKnowledgeRoot()) {
|
|
119
|
-
this.knowledgeRoot = resolve(knowledgeRoot);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
refresh(retainedPaths: Iterable<string> = this.#knownPaths): GlobalMarkdownDiscoveryResult {
|
|
123
|
-
const root = canonicalExistingPath(this.knowledgeRoot);
|
|
124
|
-
if (root === undefined) return { sources: [], removed: [], unavailable: `Knowledge root is unavailable: ${this.knowledgeRoot}` };
|
|
125
|
-
const sources = discoverGlobalMarkdownSources(root);
|
|
126
|
-
const currentPaths = new Set(sources.map((source) => source.path));
|
|
127
|
-
const removed = [...new Set(retainedPaths)]
|
|
128
|
-
.filter((path) => !currentPaths.has(path) && missingOwnedMarkdown(root, path))
|
|
129
|
-
.sort();
|
|
130
|
-
this.#knownPaths = currentPaths;
|
|
131
|
-
return { sources, removed };
|
|
132
|
-
}
|
|
133
|
-
}
|