@llblab/pi-kit 0.22.2 → 0.23.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 +7 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
- package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
- package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-state-flow/README.md +18 -6
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
- package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +7 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +32 -7
- package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/lib/skills.ts +43 -7
- package/node_modules/@llblab/pi-telegram/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
- package/package.json +6 -6
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Domain: exact file-cohort publication, current-only recovery, and cooperating worktree exclusion.
|
|
2
2
|
// Excludes: temporal algebra, Pi lifecycle, Git objects/remotes, and backend fallback policy.
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { dirname, relative, resolve } from "node:path";
|
|
7
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
8
|
import { assertOwnedFileUpdates, captureTemporalFileBases, parseScopeProvenance, parseScopeStream, restoreDurableFileBases, serializeScopeMetadata, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates, } from "./durable.js";
|
|
7
9
|
import { MAX_HISTORY_LIMIT } from "./history.js";
|
|
8
10
|
import { hashJson, sameJson } from "./json.js";
|
|
@@ -17,7 +19,7 @@ export function assertStorageDirectory(path) {
|
|
|
17
19
|
assertStorageDirectory(parent);
|
|
18
20
|
const stat = lstatSync(root, { throwIfNoEntry: false });
|
|
19
21
|
if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
|
|
20
|
-
throw new Error(`State Flow repository path is not a regular directory: ${root}`);
|
|
22
|
+
throw new Error(`State Flow repository path is not a regular directory: ${JSON.stringify(root)}`);
|
|
21
23
|
}
|
|
22
24
|
/** Explicit creation only; existing bytes and unrelated files are never adopted or rewritten here. */
|
|
23
25
|
export function initializeFileStore(root) {
|
|
@@ -27,27 +29,34 @@ export function initializeFileStore(root) {
|
|
|
27
29
|
const PUBLICATION_LOCK_WAIT_MS = 2_000;
|
|
28
30
|
const PUBLICATION_LOCK_POLL_MS = 25;
|
|
29
31
|
const publicationLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
30
|
-
function
|
|
32
|
+
function publicationLockOwner(path, allowCurrentProcess) {
|
|
31
33
|
let owner;
|
|
32
34
|
try {
|
|
33
|
-
|
|
35
|
+
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
36
|
+
if (!stat)
|
|
37
|
+
return "absent";
|
|
38
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
39
|
+
return "unavailable";
|
|
40
|
+
owner = readFileSync(path, { encoding: "utf8", flag: constants.O_RDONLY | constants.O_NOFOLLOW }).trim();
|
|
34
41
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code === "ENOENT")
|
|
44
|
+
return "absent";
|
|
45
|
+
throw error;
|
|
37
46
|
}
|
|
38
47
|
if (owner.length === 0)
|
|
39
|
-
return
|
|
48
|
+
return "pending";
|
|
40
49
|
if (!/^[1-9]\d*$/.test(owner))
|
|
41
|
-
return
|
|
50
|
+
return "unavailable";
|
|
42
51
|
const pid = Number(owner);
|
|
43
|
-
if (!Number.isSafeInteger(pid) || pid === process.pid)
|
|
44
|
-
return
|
|
52
|
+
if (!Number.isSafeInteger(pid) || (pid === process.pid && !allowCurrentProcess))
|
|
53
|
+
return "unavailable";
|
|
45
54
|
try {
|
|
46
55
|
process.kill(pid, 0);
|
|
47
|
-
return
|
|
56
|
+
return "live";
|
|
48
57
|
}
|
|
49
58
|
catch (error) {
|
|
50
|
-
return error.code === "EPERM";
|
|
59
|
+
return error.code === "EPERM" ? "live" : "unavailable";
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
/** Wait only for a cooperating live owner; interrupted or malformed locks remain explicit recovery errors. */
|
|
@@ -58,7 +67,16 @@ export function acquirePublicationLock(path, unavailable) {
|
|
|
58
67
|
return openSync(path, "wx", 0o600);
|
|
59
68
|
}
|
|
60
69
|
catch (error) {
|
|
61
|
-
if (error.code !== "EEXIST"
|
|
70
|
+
if (error.code !== "EEXIST")
|
|
71
|
+
throw unavailable(error);
|
|
72
|
+
let owner;
|
|
73
|
+
try {
|
|
74
|
+
owner = publicationLockOwner(path, false);
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
throw unavailable(cause);
|
|
78
|
+
}
|
|
79
|
+
if (owner === "unavailable" || Date.now() >= deadline)
|
|
62
80
|
throw unavailable(error);
|
|
63
81
|
Atomics.wait(publicationLockWait, 0, 0, Math.min(PUBLICATION_LOCK_POLL_MS, deadline - Date.now()));
|
|
64
82
|
}
|
|
@@ -69,7 +87,7 @@ export function withStoragePublicationLock(repositoryRoot, action) {
|
|
|
69
87
|
const root = resolve(repositoryRoot);
|
|
70
88
|
assertStorageDirectory(root);
|
|
71
89
|
const path = resolve(root, ".state-flow-publication.lock");
|
|
72
|
-
const descriptor = acquirePublicationLock(path, (cause) => new RevisionUnavailableError(`State Flow publication lock is unavailable at ${path}
|
|
90
|
+
const descriptor = acquirePublicationLock(path, (cause) => new RevisionUnavailableError(`State Flow publication lock is unavailable at ${JSON.stringify(path)}`, { cause }));
|
|
73
91
|
try {
|
|
74
92
|
writeFileSync(descriptor, `${process.pid}\n`);
|
|
75
93
|
return action(root);
|
|
@@ -79,6 +97,112 @@ export function withStoragePublicationLock(repositoryRoot, action) {
|
|
|
79
97
|
rmSync(path);
|
|
80
98
|
}
|
|
81
99
|
}
|
|
100
|
+
export class PublicationBusyError extends Error {
|
|
101
|
+
constructor(path) { super(`State Flow publication lock is busy at ${JSON.stringify(path)}`); }
|
|
102
|
+
}
|
|
103
|
+
const storageLockContext = new AsyncLocalStorage();
|
|
104
|
+
/** Await an exact file mutex; shared by canonical transactions and the independent Git backup owner. */
|
|
105
|
+
export async function withFilePublicationLock(lockPath, action, signal, unavailable = (cause) => new RevisionUnavailableError(`State Flow publication lock is unavailable at ${JSON.stringify(resolve(lockPath))}`, { cause }), waitForLock = true) {
|
|
106
|
+
const path = resolve(lockPath);
|
|
107
|
+
const inherited = storageLockContext.getStore();
|
|
108
|
+
if (inherited?.some((held) => held.active && held.path === path))
|
|
109
|
+
throw new Error(`Recursive State Flow publication lock at ${JSON.stringify(path)}`);
|
|
110
|
+
let descriptor;
|
|
111
|
+
let pendingSince;
|
|
112
|
+
while (true) {
|
|
113
|
+
signal?.throwIfAborted();
|
|
114
|
+
assertStorageDirectory(dirname(path));
|
|
115
|
+
try {
|
|
116
|
+
descriptor = openSync(path, "wx", 0o600);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.code !== "EEXIST")
|
|
121
|
+
throw unavailable(error);
|
|
122
|
+
let owner;
|
|
123
|
+
try {
|
|
124
|
+
owner = publicationLockOwner(path, true);
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
throw unavailable(cause);
|
|
128
|
+
}
|
|
129
|
+
if (owner === "unavailable")
|
|
130
|
+
throw unavailable(error);
|
|
131
|
+
if (!waitForLock)
|
|
132
|
+
throw new PublicationBusyError(path);
|
|
133
|
+
// An empty file may be between exclusive creation and PID publication, but not forever.
|
|
134
|
+
if (owner === "pending")
|
|
135
|
+
pendingSince ??= performance.now();
|
|
136
|
+
else
|
|
137
|
+
pendingSince = undefined;
|
|
138
|
+
if (pendingSince !== undefined && performance.now() - pendingSince >= PUBLICATION_LOCK_WAIT_MS)
|
|
139
|
+
throw unavailable(error);
|
|
140
|
+
}
|
|
141
|
+
await delay(PUBLICATION_LOCK_POLL_MS, undefined, { signal });
|
|
142
|
+
}
|
|
143
|
+
let identity;
|
|
144
|
+
const held = { path, active: true };
|
|
145
|
+
let failed;
|
|
146
|
+
try {
|
|
147
|
+
identity = fstatSync(descriptor);
|
|
148
|
+
writeFileSync(descriptor, `${process.pid}\n`);
|
|
149
|
+
signal?.throwIfAborted();
|
|
150
|
+
return await storageLockContext.run([...(inherited?.filter((owner) => owner.active) ?? []), held], action);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
failed = { error };
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
held.active = false;
|
|
158
|
+
try {
|
|
159
|
+
const current = lstatSync(path, { throwIfNoEntry: false });
|
|
160
|
+
if (!identity || !current?.isFile() || current.dev !== identity.dev || current.ino !== identity.ino
|
|
161
|
+
|| readFileSync(path, { encoding: "utf8", flag: constants.O_RDONLY | constants.O_NOFOLLOW }) !== `${process.pid}\n`) {
|
|
162
|
+
throw new Error(`State Flow publication lock changed during transaction at ${JSON.stringify(path)}; current owner preserved`);
|
|
163
|
+
}
|
|
164
|
+
rmSync(path);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (failed)
|
|
168
|
+
throw new AggregateError([failed.error, error], "State Flow storage transaction and lock release failed");
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
closeSync(descriptor);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Await store-wide exclusion, then capture/apply/publish through callback-scoped operations. */
|
|
177
|
+
export async function withStorageTransaction(repositoryRoot, action, signal, waitForLock = true) {
|
|
178
|
+
const root = resolve(repositoryRoot);
|
|
179
|
+
return withFilePublicationLock(resolve(root, ".state-flow-publication.lock"), async () => {
|
|
180
|
+
let active = true;
|
|
181
|
+
const guard = (requestedRoot) => {
|
|
182
|
+
if (!active)
|
|
183
|
+
throw new Error("State Flow storage transaction has ended");
|
|
184
|
+
if (resolve(requestedRoot) !== root)
|
|
185
|
+
throw new Error("State Flow storage transaction belongs to a different store");
|
|
186
|
+
signal?.throwIfAborted();
|
|
187
|
+
};
|
|
188
|
+
const transaction = Object.freeze({
|
|
189
|
+
capture: (cwd, sessionId, requestedRoot, sessionKey = sessionId) => {
|
|
190
|
+
guard(requestedRoot);
|
|
191
|
+
return { files: captureTemporalFileBases(cwd, sessionId, root, sessionKey) };
|
|
192
|
+
},
|
|
193
|
+
publish: (cwd, sessionId, view, scopes, base, requestedRoot, runtime, sessionKey, provenance, runtimeOnly) => {
|
|
194
|
+
guard(requestedRoot);
|
|
195
|
+
return publishLockedTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey, provenance, runtimeOnly);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
return await action(transaction);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
active = false;
|
|
203
|
+
}
|
|
204
|
+
}, signal, undefined, waitForLock);
|
|
205
|
+
}
|
|
82
206
|
export function assertTemporalFileBase(expected, current) {
|
|
83
207
|
if (expected.files.length !== current.files.length || current.files.some((file, index) => {
|
|
84
208
|
const previous = expected.files[index];
|
|
@@ -191,16 +315,18 @@ export function loadTemporalFileRevision(cwd, sessionId, root, revision, session
|
|
|
191
315
|
}
|
|
192
316
|
/** Publish a validated canonical cohort; runtimeOnly owns only session config/runtime files. */
|
|
193
317
|
export function publishTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey = sessionId, provenance, runtimeOnly = false) {
|
|
194
|
-
return withStoragePublicationLock(root, (locked) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
318
|
+
return withStoragePublicationLock(root, (locked) => publishLockedTemporalStateToFiles(cwd, sessionId, view, scopes, base, locked, runtime, sessionKey, provenance, runtimeOnly));
|
|
319
|
+
}
|
|
320
|
+
/** Shared publication owner; callers must hold the matching store lock for the complete cohort. */
|
|
321
|
+
function publishLockedTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey = sessionId, provenance, runtimeOnly = false) {
|
|
322
|
+
const current = { files: captureTemporalFileBases(cwd, sessionId, root, sessionKey) };
|
|
323
|
+
assertTemporalFileBase(base, current);
|
|
324
|
+
const { updates } = planTemporalPublication(cwd, sessionId, view, scopes, current, root, runtime, runtimeOnly, sessionKey, provenance);
|
|
325
|
+
const next = { files: temporalFileReceipts(current, updates) };
|
|
326
|
+
decodeFileCohort(cwd, sessionId, root, next, sessionKey);
|
|
327
|
+
const revision = fileRevision(next, root);
|
|
328
|
+
publishFileUpdates(current.files, updates, root);
|
|
329
|
+
return { base: next, revision, changed: updates.length > 0 };
|
|
204
330
|
}
|
|
205
331
|
function publishFileUpdates(bases, updates, root) {
|
|
206
332
|
writeOwnedFileUpdates(updates, bases, root);
|
|
@@ -78,6 +78,14 @@ export type StateFlowTelegramLoader = () => Promise<StateFlowTelegramModules>;
|
|
|
78
78
|
export interface StateFlowTelegramControlResult {
|
|
79
79
|
ok: boolean;
|
|
80
80
|
message: string;
|
|
81
|
+
/** A completed control can lose its presentation authority after a mode or selection change. */
|
|
82
|
+
signal?: AbortSignal;
|
|
83
|
+
}
|
|
84
|
+
export interface StateFlowTelegramInspection {
|
|
85
|
+
state: StateFlowTelegramState;
|
|
86
|
+
revisions: ScopeRevisions;
|
|
87
|
+
/** A selection can revoke a completed observation before the adapter presents it. */
|
|
88
|
+
signal?: AbortSignal;
|
|
81
89
|
}
|
|
82
90
|
export interface StateFlowTelegramPort {
|
|
83
91
|
snapshot(): StateFlowTelegramSnapshot;
|
|
@@ -90,6 +98,11 @@ export interface StateFlowTelegramPort {
|
|
|
90
98
|
deferStart(): void;
|
|
91
99
|
cancelStart(): void;
|
|
92
100
|
}
|
|
101
|
+
export interface StateFlowTelegramInspectionPort extends Omit<StateFlowTelegramPort, "state" | "revisions" | "start" | "stop"> {
|
|
102
|
+
inspect(scope: StateFlowTelegramScope): StateFlowTelegramInspection | Promise<StateFlowTelegramInspection>;
|
|
103
|
+
start(): StateFlowTelegramControlResult | Promise<StateFlowTelegramControlResult>;
|
|
104
|
+
stop(): StateFlowTelegramControlResult | Promise<StateFlowTelegramControlResult>;
|
|
105
|
+
}
|
|
93
106
|
export interface StateFlowTelegramAdapter {
|
|
94
107
|
ensure(): Promise<boolean>;
|
|
95
108
|
dispose(): void;
|
|
@@ -103,6 +116,6 @@ export declare function renderStateFlowRichState(scope: StateFlowTelegramScope,
|
|
|
103
116
|
/** Default loader; injectable so tests and embedded hosts can control transport presence. */
|
|
104
117
|
export declare function loadStateFlowTelegramModules(): Promise<StateFlowTelegramModules>;
|
|
105
118
|
export declare function createStateFlowTelegramAdapter(options: {
|
|
106
|
-
port: StateFlowTelegramPort;
|
|
119
|
+
port: StateFlowTelegramPort | StateFlowTelegramInspectionPort;
|
|
107
120
|
load?: StateFlowTelegramLoader;
|
|
108
121
|
}): StateFlowTelegramAdapter;
|
|
@@ -10,6 +10,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
}
|
|
11
11
|
return path;
|
|
12
12
|
};
|
|
13
|
+
import { conciseDiagnostic, diagnosticText } from "./protocol.js";
|
|
13
14
|
import { formatScopeRevisionVector } from "./status.js";
|
|
14
15
|
export const STATE_FLOW_TELEGRAM_ID = "@llblab/pi-state-flow";
|
|
15
16
|
/** Resolve the package export or the compiled sibling-extension layout used in local development. */
|
|
@@ -124,7 +125,8 @@ export function renderStateFlowRichState(scope, revisions, state) {
|
|
|
124
125
|
function isStateFlowTelegramScope(value) {
|
|
125
126
|
return value === "global" || value === "cwd" || value === "session" || value === "effective";
|
|
126
127
|
}
|
|
127
|
-
function buildStateFlowTelegramSection(port) {
|
|
128
|
+
function buildStateFlowTelegramSection(port, isActive) {
|
|
129
|
+
let interaction = 0;
|
|
128
130
|
return {
|
|
129
131
|
id: STATE_FLOW_TELEGRAM_ID,
|
|
130
132
|
label: "🌀 State Flow",
|
|
@@ -134,7 +136,9 @@ function buildStateFlowTelegramSection(port) {
|
|
|
134
136
|
// cancel/refresh remain routable for keyboards sent by earlier versions.
|
|
135
137
|
if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh" && ctx.action !== "show-state" && ctx.action !== "inspect" && ctx.action !== "back")
|
|
136
138
|
return "pass";
|
|
139
|
+
const request = ++interaction;
|
|
137
140
|
let notice;
|
|
141
|
+
let acknowledged = false;
|
|
138
142
|
try {
|
|
139
143
|
if (ctx.action === "show-state") {
|
|
140
144
|
await ctx.answerCallback();
|
|
@@ -144,23 +148,44 @@ function buildStateFlowTelegramSection(port) {
|
|
|
144
148
|
if (ctx.action === "inspect") {
|
|
145
149
|
if (!isStateFlowTelegramScope(ctx.payload))
|
|
146
150
|
throw new Error("Unknown State Flow scope");
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
let observation;
|
|
152
|
+
if ("inspect" in port) {
|
|
153
|
+
await ctx.answerCallback("Reading State Flow memory");
|
|
154
|
+
acknowledged = true;
|
|
155
|
+
observation = await port.inspect(ctx.payload);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const state = port.state(ctx.payload);
|
|
159
|
+
const live = port.snapshot();
|
|
160
|
+
observation = { state, revisions: port.revisions?.() ?? live.revisions ?? { global: live.step, cwd: live.step, session: live.step } };
|
|
161
|
+
}
|
|
162
|
+
observation.signal?.throwIfAborted();
|
|
163
|
+
if (request !== interaction || !isActive())
|
|
164
|
+
return "handled";
|
|
165
|
+
await ctx.openRich(renderStateFlowRichState(ctx.payload, observation.revisions, observation.state));
|
|
166
|
+
if (!acknowledged)
|
|
167
|
+
await ctx.answerCallback();
|
|
152
168
|
return "handled";
|
|
153
169
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
const action = ctx.action === "stop" || (ctx.action === "start" && port.canStartNow()) ? ctx.action : undefined;
|
|
171
|
+
if (action) {
|
|
172
|
+
if ("inspect" in port) {
|
|
173
|
+
acknowledged = true;
|
|
174
|
+
// Start the control immediately and acknowledge in parallel; neither promise can reject unobserved.
|
|
175
|
+
const [, result] = await Promise.all([
|
|
176
|
+
ctx.answerCallback(action === "stop" ? "Stopping State Flow" : "Starting State Flow"),
|
|
177
|
+
Promise.resolve().then(() => port[action]()),
|
|
178
|
+
]);
|
|
179
|
+
if (result.signal?.aborted)
|
|
180
|
+
return "handled";
|
|
181
|
+
notice = result.message;
|
|
160
182
|
}
|
|
183
|
+
else
|
|
184
|
+
notice = port[action]().message;
|
|
161
185
|
}
|
|
162
|
-
else if (ctx.action === "
|
|
163
|
-
|
|
186
|
+
else if (ctx.action === "start") {
|
|
187
|
+
port.deferStart();
|
|
188
|
+
notice = "State Flow will start after the current turn";
|
|
164
189
|
}
|
|
165
190
|
else if (ctx.action === "cancel") {
|
|
166
191
|
port.cancelStart();
|
|
@@ -168,10 +193,19 @@ function buildStateFlowTelegramSection(port) {
|
|
|
168
193
|
}
|
|
169
194
|
}
|
|
170
195
|
catch (error) {
|
|
171
|
-
notice =
|
|
196
|
+
notice = diagnosticText(error);
|
|
197
|
+
}
|
|
198
|
+
if (request !== interaction || !isActive())
|
|
199
|
+
return "handled";
|
|
200
|
+
const summary = notice === undefined ? undefined : conciseDiagnostic(notice, 200);
|
|
201
|
+
const view = buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action));
|
|
202
|
+
if (acknowledged && summary !== undefined) {
|
|
203
|
+
// Callback queries can expire during storage waits; retain errors in the existing menu instead.
|
|
204
|
+
view.text += `\n\n${summary.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")}`;
|
|
172
205
|
}
|
|
173
|
-
|
|
174
|
-
|
|
206
|
+
else if (!acknowledged)
|
|
207
|
+
await ctx.answerCallback(summary);
|
|
208
|
+
await ctx.edit(view);
|
|
175
209
|
return "handled";
|
|
176
210
|
},
|
|
177
211
|
};
|
|
@@ -214,7 +248,7 @@ export function createStateFlowTelegramAdapter(options) {
|
|
|
214
248
|
return false;
|
|
215
249
|
if (!sectionRegistered && modules.sections) {
|
|
216
250
|
try {
|
|
217
|
-
const dispose = modules.sections.registerTelegramSection(buildStateFlowTelegramSection(options.port));
|
|
251
|
+
const dispose = modules.sections.registerTelegramSection(buildStateFlowTelegramSection(options.port, () => epoch === generation));
|
|
218
252
|
if (epoch === generation) {
|
|
219
253
|
disposers.push(dispose);
|
|
220
254
|
sectionRegistered = true;
|
|
@@ -31,16 +31,16 @@ function validateSkillCompilerOutput(scope, path, output) {
|
|
|
31
31
|
problems.push("artifact entry is missing");
|
|
32
32
|
else {
|
|
33
33
|
if (typeof output.description !== "string" || output.description.trim().length === 0)
|
|
34
|
-
problems.push("description must be
|
|
34
|
+
problems.push("description must be non-empty");
|
|
35
35
|
if (output.kind !== "skill")
|
|
36
36
|
problems.push('kind must be "skill"');
|
|
37
37
|
if (!isObject(output.compilation) || Object.keys(output.compilation).length === 0)
|
|
38
|
-
problems.push("compilation must be
|
|
38
|
+
problems.push("compilation object must be non-empty");
|
|
39
39
|
}
|
|
40
40
|
if (problems.length === 0)
|
|
41
41
|
return;
|
|
42
42
|
const target = `${scope}.artifacts[${JSON.stringify(path)}]`;
|
|
43
|
-
throw new Error(`Skill
|
|
43
|
+
throw new Error(`Invalid Skill at ${target}: ${problems.join("; ")}`);
|
|
44
44
|
}
|
|
45
45
|
function validateSkillCompilerTargets(patches, successfulSkillReads) {
|
|
46
46
|
for (const read of successfulSkillReads) {
|
|
@@ -64,7 +64,7 @@ function compileReadSkills(scope, nextState, patch, successfulSkillReads, proven
|
|
|
64
64
|
const output = patch.artifacts[read.path];
|
|
65
65
|
validateSkillCompilerOutput(scope, read.path, output);
|
|
66
66
|
const compiled = compileArtifact({
|
|
67
|
-
source: { path: read.path, hash: read.hash },
|
|
67
|
+
source: { path: read.path, scope, hash: read.hash },
|
|
68
68
|
compiler: SKILL_ARTIFACT_COMPILER,
|
|
69
69
|
output: { ...structuredClone(output), kind: "skill" },
|
|
70
70
|
});
|
|
@@ -81,11 +81,11 @@ function compileReadSkills(scope, nextState, patch, successfulSkillReads, proven
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
function validateMaterializedTransition(nextState) {
|
|
84
|
+
function validateMaterializedTransition(nextState, scope) {
|
|
85
85
|
if (containsNull(nextState)) {
|
|
86
86
|
throw new Error("Materialized state cannot contain null; use null only as an object-key deletion marker");
|
|
87
87
|
}
|
|
88
|
-
validateArtifactRegistry(nextState.artifacts);
|
|
88
|
+
validateArtifactRegistry(nextState.artifacts, `${scope}.artifacts`);
|
|
89
89
|
if (Object.hasOwn(nextState.contract, "compiled_skills")) {
|
|
90
90
|
throw new Error("contract.compiled_skills is retired; Skill compilations belong only in source-addressed artifacts");
|
|
91
91
|
}
|
|
@@ -109,7 +109,7 @@ function validateScopePatch(scope, patch) {
|
|
|
109
109
|
throw new Error("Scoped State Flow patch field lazy must be a JSON object");
|
|
110
110
|
}
|
|
111
111
|
if (isObject(patch.artifacts))
|
|
112
|
-
validateModelArtifactPatch(patch.artifacts);
|
|
112
|
+
validateModelArtifactPatch(patch.artifacts, `${scope}.artifacts`);
|
|
113
113
|
}
|
|
114
114
|
function completePatch(patch, response) {
|
|
115
115
|
return {
|
|
@@ -153,7 +153,7 @@ function stageScopedSemanticTransition(currentStates, transition, successfulSkil
|
|
|
153
153
|
const nextState = applyPatch(currentStates[scope], patch);
|
|
154
154
|
compileReadArtifacts(nextState, { artifacts: authored.artifacts ?? {} }, artifactReads.filter((read) => (read.scope ?? "global") === scope), provenanceUpdates[scope]);
|
|
155
155
|
compileReadSkills(scope, nextState, { artifacts: authored.artifacts ?? {} }, skillReads.filter((read) => read.scope === scope), provenanceUpdates[scope]);
|
|
156
|
-
validateMaterializedTransition(nextState);
|
|
156
|
+
validateMaterializedTransition(nextState, scope);
|
|
157
157
|
nextStates[scope] = nextState;
|
|
158
158
|
}
|
|
159
159
|
return {
|
|
@@ -48,7 +48,9 @@ Unscoped paths use effective state. `cwd[1].working` reads the preceding causal
|
|
|
48
48
|
|
|
49
49
|
## Write
|
|
50
50
|
|
|
51
|
-
Call `patch_state` alone per assistant response; await acceptance before dependent work. Supply one or more of `global`, `cwd`, and `session`; supplied scopes commit atomically. Omit unchanged scopes.
|
|
51
|
+
Call `patch_state` alone per assistant response; await acceptance before dependent work. Supply one or more of `global`, `cwd`, and `session`; supplied scopes commit atomically. Default to Session for current work, CWD for reusable project knowledge, and Global for established cross-project knowledge. Omit unchanged scopes.
|
|
52
|
+
|
|
53
|
+
The runtime waits cancelably for publication ownership, then applies authored Global/CWD operations to current canonical values. Untouched fields survive; overlapping targets follow successful acceptance order. Correct repeats succeed as `State already current.` without another semantic revision. Do not repeat external actions during a memory wait, or rebuild an entire scope from an older snapshot. Session ownership/history fences remain private, not a universal merge.
|
|
52
54
|
|
|
53
55
|
Semantic planes `intents`, `contract`, `working`, `artifacts`, and the required `lazy` root are objects; nested lazy values may contain ordinary JSON without stored nulls. Objects merge, arrays/scalars replace, omitted fields persist. Nested `null` removes an owned object key; inherited content may reappear. Canonical `"[N]"` keys patch array elements; indexed deletion is forbidden.
|
|
54
56
|
|