@llblab/pi-kit 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -7
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +8 -9
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +30 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -3
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/index.js +20 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +78 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +110 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +334 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +49 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +67 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +53 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +23 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +109 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +111 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +189 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +125 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +102 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +507 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +22 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1263 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +72 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +565 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +22 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +12 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +109 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +24 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +36 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +98 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +15 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +42 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +13 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +133 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +59 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +69 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +335 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +35 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +36 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +142 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +529 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +44 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +131 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +255 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +55 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +31 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +46 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +217 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +100 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +234 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +203 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +204 -0
- package/node_modules/@llblab/pi-state-flow/dist/package.json +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/pi-state-flow/index.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +138 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -5
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +5 -1
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +6 -4
- package/node_modules/@llblab/pi-state-flow/index.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +17 -1
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +88 -96
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +64 -12
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -188
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +84 -48
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +40 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +7 -30
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +48 -50
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +41 -97
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +17 -12
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +13 -9
- package/node_modules/@llblab/pi-state-flow/package.json +23 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +3 -1
- package/package.json +4 -4
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Domain: optional pi-telegram presentation adapter for the State Flow main-menu section.
|
|
2
|
+
//
|
|
3
|
+
// This is a leaf adapter. Core semantics, storage, and inference never depend on it; when
|
|
4
|
+
// pi-telegram is absent or its registry is not ready, registration fails open and retries.
|
|
5
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
6
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
7
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
8
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return path;
|
|
12
|
+
};
|
|
13
|
+
export const STATE_FLOW_TELEGRAM_ID = "@llblab/pi-state-flow";
|
|
14
|
+
/** Resolve the package export or the compiled sibling-extension layout used in local development. */
|
|
15
|
+
export function stateFlowTelegramSectionSpecifiers(moduleUrl = import.meta.url) {
|
|
16
|
+
return [
|
|
17
|
+
"@llblab/pi-telegram/sections",
|
|
18
|
+
new URL("../../../pi-telegram/dist/api/sections.js", moduleUrl).href,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
/** Main-menu section label doubles as the live status value: the spiral identity is constant, the value is not. */
|
|
22
|
+
export function formatStateFlowSectionLabel(snapshot) {
|
|
23
|
+
return `🌀 State Flow: ${stateFlowLabelValue(snapshot)}`;
|
|
24
|
+
}
|
|
25
|
+
/** Shared live value: plain in the button label, monospaced in the submenu state line. */
|
|
26
|
+
function stateFlowLabelValue(snapshot) {
|
|
27
|
+
return snapshot.enabled ? `#${snapshot.step}` : "off";
|
|
28
|
+
}
|
|
29
|
+
/** Submenu state line: the same identity as the button label, with the live value in monospace. */
|
|
30
|
+
function formatStateFlowSectionHeader(snapshot) {
|
|
31
|
+
return `<b>🌀 State Flow: <code>${stateFlowLabelValue(snapshot)}</code></b>`;
|
|
32
|
+
}
|
|
33
|
+
/** Short help under the state line: what State Flow is and why its action button exists. */
|
|
34
|
+
const STATE_FLOW_SECTION_HELP = "Records the latest accepted state after every turn, so a new session resumes from the last committed point.";
|
|
35
|
+
/** The submenu header repeats the button's state line; the single action matches the current state. */
|
|
36
|
+
export function buildStateFlowSectionView(snapshot, callbackData) {
|
|
37
|
+
const action = snapshot.enabled
|
|
38
|
+
? { text: "⏹ Stop", callback_data: callbackData("stop") }
|
|
39
|
+
: { text: "▶️ Start", callback_data: callbackData("start") };
|
|
40
|
+
return {
|
|
41
|
+
text: [formatStateFlowSectionHeader(snapshot), "", STATE_FLOW_SECTION_HELP].join("\n"),
|
|
42
|
+
parseMode: "html",
|
|
43
|
+
replyMarkup: { inline_keyboard: [
|
|
44
|
+
[action],
|
|
45
|
+
[{ text: "👁 Show state", callback_data: callbackData("show-state") }],
|
|
46
|
+
] },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const STATE_FLOW_SCOPE_LABELS = {
|
|
50
|
+
global: "🌐 Global",
|
|
51
|
+
cwd: "📂 CWD",
|
|
52
|
+
session: "💬 Session",
|
|
53
|
+
effective: "🧬 Effective",
|
|
54
|
+
};
|
|
55
|
+
export function buildStateFlowScopeChooser(callbackData) {
|
|
56
|
+
return {
|
|
57
|
+
text: "<b>👁 Show state:</b>",
|
|
58
|
+
parseMode: "html",
|
|
59
|
+
replyMarkup: { inline_keyboard: [
|
|
60
|
+
...["global", "cwd", "session", "effective"].map((scope) => [
|
|
61
|
+
{ text: STATE_FLOW_SCOPE_LABELS[scope], callback_data: callbackData("inspect", scope) },
|
|
62
|
+
]),
|
|
63
|
+
] },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// The complete message serializes each preformatted field one additional time;
|
|
67
|
+
// 3,000 leaves safe headroom for worst-case JSON escaping across all four fields.
|
|
68
|
+
const STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS = 3_000;
|
|
69
|
+
function renderStateFlowTelegramField(value) {
|
|
70
|
+
const json = JSON.stringify(value, null, 2);
|
|
71
|
+
if (json.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS)
|
|
72
|
+
return json;
|
|
73
|
+
let low = 0;
|
|
74
|
+
let high = json.length;
|
|
75
|
+
let rendered = "";
|
|
76
|
+
while (low <= high) {
|
|
77
|
+
const length = Math.floor((low + high) / 2);
|
|
78
|
+
const candidate = JSON.stringify({
|
|
79
|
+
truncated: true,
|
|
80
|
+
preview: json.slice(0, length),
|
|
81
|
+
omittedChars: json.length - length,
|
|
82
|
+
}, null, 2);
|
|
83
|
+
if (candidate.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
84
|
+
rendered = candidate;
|
|
85
|
+
low = length + 1;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
high = length - 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return rendered;
|
|
92
|
+
}
|
|
93
|
+
export function renderStateFlowRichState(scope, step, state) {
|
|
94
|
+
const fields = ["artifacts", "contract", "working", "response"];
|
|
95
|
+
return {
|
|
96
|
+
blocks: [
|
|
97
|
+
{ type: "heading", text: `${STATE_FLOW_SCOPE_LABELS[scope]}: \`#${step}\``, size: 3 },
|
|
98
|
+
...fields.map((field) => ({
|
|
99
|
+
type: "details",
|
|
100
|
+
summary: { type: "code", text: field },
|
|
101
|
+
blocks: [{ type: "pre", language: "json", text: renderStateFlowTelegramField(state[field]) }],
|
|
102
|
+
})),
|
|
103
|
+
],
|
|
104
|
+
skip_entity_detection: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function isStateFlowTelegramScope(value) {
|
|
108
|
+
return value === "global" || value === "cwd" || value === "session" || value === "effective";
|
|
109
|
+
}
|
|
110
|
+
function buildStateFlowTelegramSection(port) {
|
|
111
|
+
return {
|
|
112
|
+
id: STATE_FLOW_TELEGRAM_ID,
|
|
113
|
+
label: "🌀 State Flow",
|
|
114
|
+
getLabel: () => formatStateFlowSectionLabel(port.snapshot()),
|
|
115
|
+
render: (ctx) => buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action)),
|
|
116
|
+
handleCallback: async (ctx) => {
|
|
117
|
+
// cancel/refresh remain routable for keyboards sent by earlier versions.
|
|
118
|
+
if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh" && ctx.action !== "show-state" && ctx.action !== "inspect" && ctx.action !== "back")
|
|
119
|
+
return "pass";
|
|
120
|
+
let notice;
|
|
121
|
+
try {
|
|
122
|
+
if (ctx.action === "show-state") {
|
|
123
|
+
await ctx.answerCallback();
|
|
124
|
+
await ctx.edit(buildStateFlowScopeChooser((action, payload) => ctx.callbackData(action, payload)));
|
|
125
|
+
return "handled";
|
|
126
|
+
}
|
|
127
|
+
if (ctx.action === "inspect") {
|
|
128
|
+
if (!isStateFlowTelegramScope(ctx.payload))
|
|
129
|
+
throw new Error("Unknown State Flow scope");
|
|
130
|
+
await ctx.openRich(renderStateFlowRichState(ctx.payload, port.snapshot().step, port.state(ctx.payload)));
|
|
131
|
+
await ctx.answerCallback();
|
|
132
|
+
return "handled";
|
|
133
|
+
}
|
|
134
|
+
if (ctx.action === "start") {
|
|
135
|
+
if (port.canStartNow())
|
|
136
|
+
notice = port.start().message;
|
|
137
|
+
else {
|
|
138
|
+
port.deferStart();
|
|
139
|
+
notice = "State Flow will start after the current turn";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (ctx.action === "stop") {
|
|
143
|
+
notice = port.stop().message;
|
|
144
|
+
}
|
|
145
|
+
else if (ctx.action === "cancel") {
|
|
146
|
+
port.cancelStart();
|
|
147
|
+
notice = "Pending start cancelled";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
152
|
+
}
|
|
153
|
+
await ctx.answerCallback(notice);
|
|
154
|
+
await ctx.edit(buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action)));
|
|
155
|
+
return "handled";
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function importTelegramModule(specifiers, guard) {
|
|
160
|
+
for (const specifier of specifiers) {
|
|
161
|
+
try {
|
|
162
|
+
const imported = await import(__rewriteRelativeImportExtension(specifier));
|
|
163
|
+
if (guard(imported))
|
|
164
|
+
return imported;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// pi-telegram is optional; its absence only disables the Telegram surface.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
/** Default loader; injectable so tests and embedded hosts can control transport presence. */
|
|
173
|
+
export async function loadStateFlowTelegramModules() {
|
|
174
|
+
const sections = await importTelegramModule(stateFlowTelegramSectionSpecifiers(), (module) => typeof module?.registerTelegramSection === "function");
|
|
175
|
+
return { ...(sections === undefined ? {} : { sections }) };
|
|
176
|
+
}
|
|
177
|
+
export function createStateFlowTelegramAdapter(options) {
|
|
178
|
+
const load = options.load ?? loadStateFlowTelegramModules;
|
|
179
|
+
let generation = 0;
|
|
180
|
+
let sectionRegistered = false;
|
|
181
|
+
let registration;
|
|
182
|
+
const disposers = [];
|
|
183
|
+
const register = async () => {
|
|
184
|
+
const epoch = generation;
|
|
185
|
+
let modules;
|
|
186
|
+
try {
|
|
187
|
+
modules = await load();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
// A shutdown during loading must not leave a registration behind.
|
|
193
|
+
if (epoch !== generation)
|
|
194
|
+
return false;
|
|
195
|
+
if (!sectionRegistered && modules.sections) {
|
|
196
|
+
try {
|
|
197
|
+
const dispose = modules.sections.registerTelegramSection(buildStateFlowTelegramSection(options.port));
|
|
198
|
+
if (epoch === generation) {
|
|
199
|
+
disposers.push(dispose);
|
|
200
|
+
sectionRegistered = true;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
dispose();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Registry not initialized yet; the next ensure retries.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return sectionRegistered;
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
async ensure() {
|
|
214
|
+
if (sectionRegistered)
|
|
215
|
+
return true;
|
|
216
|
+
registration ??= register().finally(() => {
|
|
217
|
+
registration = undefined;
|
|
218
|
+
});
|
|
219
|
+
return registration;
|
|
220
|
+
},
|
|
221
|
+
dispose() {
|
|
222
|
+
generation += 1;
|
|
223
|
+
for (const dispose of disposers.splice(0)) {
|
|
224
|
+
try {
|
|
225
|
+
dispose();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Disposal is best-effort; pi-telegram owns its registry lifetime.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
sectionRegistered = false;
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type RecentScopePatch } from "./history.ts";
|
|
2
|
+
import { type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
3
|
+
/** Owns hot temporal algebra; excludes filesystem, Git, identity allocation, and Pi lifecycle. */
|
|
4
|
+
export interface TransitionBoundary {
|
|
5
|
+
id: string;
|
|
6
|
+
/** Branch-local order only. Identity and parent links distinguish forks at equal positions. */
|
|
7
|
+
position: number;
|
|
8
|
+
parent: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ScopeCheckpoint {
|
|
11
|
+
through: TransitionBoundary;
|
|
12
|
+
state: MaterializedState;
|
|
13
|
+
}
|
|
14
|
+
export interface TemporalPatch {
|
|
15
|
+
transition: TransitionBoundary;
|
|
16
|
+
patch: RecentScopePatch["patch"];
|
|
17
|
+
}
|
|
18
|
+
export interface ScopeStream {
|
|
19
|
+
checkpoint: ScopeCheckpoint;
|
|
20
|
+
patches: TemporalPatch[];
|
|
21
|
+
}
|
|
22
|
+
export interface TemporalState {
|
|
23
|
+
/** Oldest to newest, including the boundary immediately before the retained transitions. */
|
|
24
|
+
lineage: TransitionBoundary[];
|
|
25
|
+
scopes: Record<StateScope, ScopeStream>;
|
|
26
|
+
}
|
|
27
|
+
/** Replay validation is shared by disk codecs and active-lineage materialization. */
|
|
28
|
+
export declare function validateScopeStream(value: unknown, scope: StateScope): asserts value is ScopeStream;
|
|
29
|
+
export declare function validateTemporalLineage(value: unknown): asserts value is TransitionBoundary[];
|
|
30
|
+
/** Validate one revision-selected cohort. Its older ancestry must be bound by the durable loader. */
|
|
31
|
+
export declare function validateTemporalState(view: TemporalState): void;
|
|
32
|
+
/** Adopt revision-proven inherited streams without rewriting their checkpoints or tails. */
|
|
33
|
+
export declare function adoptTemporalStreams(scopes: Record<StateScope, ScopeStream>, id: string): TemporalState;
|
|
34
|
+
/** New or migrated state starts at a proven current boundary, with no invented past. */
|
|
35
|
+
export declare function createTemporalState(states: ScopedStates, id: string): TemporalState;
|
|
36
|
+
/** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
|
|
37
|
+
export declare function readTemporalState(view: TemporalState, offset?: number, scope?: StateScope): MaterializedState;
|
|
38
|
+
/** Allocate the identity outside this algebra; only materially effective patches accept it. */
|
|
39
|
+
export declare function advanceTemporalState(view: TemporalState, transitions: readonly RecentScopePatch[], id: string): TemporalState;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { RECENT_TRANSITION_LIMIT, validateRecentTransition } from "./history.js";
|
|
2
|
+
import { applyPatch, containsNull, isJsonValue, isObject, sameJson } from "./json.js";
|
|
3
|
+
import { isMaterializedState, overlayStates } from "./state.js";
|
|
4
|
+
const SCOPES = ["global", "cwd", "session"];
|
|
5
|
+
function validateBoundary(boundary) {
|
|
6
|
+
if (!isObject(boundary) || Object.keys(boundary).sort().join(",") !== "id,parent,position"
|
|
7
|
+
|| typeof boundary.id !== "string" || boundary.id.trim().length === 0
|
|
8
|
+
|| !Number.isSafeInteger(boundary.position) || boundary.position < 0
|
|
9
|
+
|| (boundary.parent !== null && (typeof boundary.parent !== "string" || boundary.parent.trim().length === 0))
|
|
10
|
+
|| boundary.parent === boundary.id || (boundary.position === 0 && boundary.parent !== null)) {
|
|
11
|
+
throw new Error("Invalid State Flow temporal boundary");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function validateState(state) {
|
|
15
|
+
if (!isJsonValue(state) || !isMaterializedState(state) || containsNull(state)) {
|
|
16
|
+
throw new Error("Invalid temporal materialized semantic state");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function apply(state, patch) {
|
|
20
|
+
const next = applyPatch(state, patch);
|
|
21
|
+
validateState(next);
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
function sameBoundary(left, right) {
|
|
25
|
+
return left.id === right.id && left.position === right.position && left.parent === right.parent;
|
|
26
|
+
}
|
|
27
|
+
/** Replay validation is shared by disk codecs and active-lineage materialization. */
|
|
28
|
+
export function validateScopeStream(value, scope) {
|
|
29
|
+
if (!SCOPES.includes(scope))
|
|
30
|
+
throw new Error("Unknown temporal scope");
|
|
31
|
+
if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "checkpoint,patches"
|
|
32
|
+
|| !isObject(value.checkpoint) || Object.keys(value.checkpoint).sort().join(",") !== "state,through"
|
|
33
|
+
|| !Array.isArray(value.patches)) {
|
|
34
|
+
throw new Error("Invalid temporal checkpoint/tail envelope");
|
|
35
|
+
}
|
|
36
|
+
const stream = value;
|
|
37
|
+
validateBoundary(stream.checkpoint.through);
|
|
38
|
+
validateState(stream.checkpoint.state);
|
|
39
|
+
if (stream.patches.length > RECENT_TRANSITION_LIMIT)
|
|
40
|
+
throw new Error("Temporal scope tail exceeds seven patches");
|
|
41
|
+
let previous = stream.checkpoint.through;
|
|
42
|
+
let state = stream.checkpoint.state;
|
|
43
|
+
const identities = new Set([previous.id]);
|
|
44
|
+
for (const record of stream.patches) {
|
|
45
|
+
if (!isObject(record) || Object.keys(record).sort().join(",") !== "patch,transition") {
|
|
46
|
+
throw new Error("Invalid temporal patch envelope");
|
|
47
|
+
}
|
|
48
|
+
validateBoundary(record.transition);
|
|
49
|
+
if (record.transition.position <= previous.position || identities.has(record.transition.id)) {
|
|
50
|
+
throw new Error("Temporal scope tail is not ordered after its checkpoint");
|
|
51
|
+
}
|
|
52
|
+
if (record.transition.position === previous.position + 1 && record.transition.parent !== previous.id) {
|
|
53
|
+
throw new Error("Disconnected State Flow temporal ancestry");
|
|
54
|
+
}
|
|
55
|
+
validateRecentTransition({ id: record.transition.id, at: 0, transitions: [{ scope, patch: record.patch }] });
|
|
56
|
+
const next = apply(state, record.patch);
|
|
57
|
+
if (sameJson(next, state))
|
|
58
|
+
throw new Error("Temporal scope tail contains a semantic no-op");
|
|
59
|
+
state = next;
|
|
60
|
+
previous = record.transition;
|
|
61
|
+
identities.add(previous.id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function validateTemporalLineage(value) {
|
|
65
|
+
if (!isJsonValue(value) || !Array.isArray(value) || value.length === 0 || value.length > RECENT_TRANSITION_LIMIT + 1) {
|
|
66
|
+
throw new Error("Temporal lineage must contain between one and eight boundaries");
|
|
67
|
+
}
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (let index = 0; index < value.length; index++) {
|
|
70
|
+
const boundary = value[index];
|
|
71
|
+
validateBoundary(boundary);
|
|
72
|
+
if (seen.has(boundary.id))
|
|
73
|
+
throw new Error("Duplicate State Flow temporal boundary");
|
|
74
|
+
seen.add(boundary.id);
|
|
75
|
+
const previous = value[index - 1];
|
|
76
|
+
if (previous && (boundary.position !== previous.position + 1 || boundary.parent !== previous.id)) {
|
|
77
|
+
throw new Error("Disconnected State Flow temporal lineage");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Validate one revision-selected cohort. Its older ancestry must be bound by the durable loader. */
|
|
82
|
+
export function validateTemporalState(view) {
|
|
83
|
+
validateTemporalLineage(view.lineage);
|
|
84
|
+
const oldest = view.lineage[0];
|
|
85
|
+
const identities = new Map();
|
|
86
|
+
const positions = new Map();
|
|
87
|
+
const remember = (boundary) => {
|
|
88
|
+
validateBoundary(boundary);
|
|
89
|
+
const identity = identities.get(boundary.id);
|
|
90
|
+
const position = boundary.position >= oldest.position ? positions.get(boundary.position) : undefined;
|
|
91
|
+
if ((identity && !sameBoundary(identity, boundary)) || (position && !sameBoundary(position, boundary))) {
|
|
92
|
+
throw new Error("Conflicting State Flow temporal lineage");
|
|
93
|
+
}
|
|
94
|
+
identities.set(boundary.id, boundary);
|
|
95
|
+
if (boundary.position >= oldest.position)
|
|
96
|
+
positions.set(boundary.position, boundary);
|
|
97
|
+
};
|
|
98
|
+
for (const boundary of view.lineage)
|
|
99
|
+
remember(boundary);
|
|
100
|
+
const head = view.lineage.at(-1);
|
|
101
|
+
for (const scope of SCOPES) {
|
|
102
|
+
const stream = view.scopes[scope];
|
|
103
|
+
validateScopeStream(stream, scope);
|
|
104
|
+
remember(stream.checkpoint.through);
|
|
105
|
+
if (stream.checkpoint.through.position > oldest.position) {
|
|
106
|
+
throw new Error("Scope checkpoint is newer than the guaranteed hot boundary");
|
|
107
|
+
}
|
|
108
|
+
for (const record of stream.patches) {
|
|
109
|
+
remember(record.transition);
|
|
110
|
+
if (record.transition.position > head.position)
|
|
111
|
+
throw new Error("Temporal scope patch is beyond the active head");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const boundary of identities.values()) {
|
|
115
|
+
const previous = positions.get(boundary.position - 1);
|
|
116
|
+
if (previous && boundary.parent !== previous.id)
|
|
117
|
+
throw new Error("Disconnected State Flow temporal ancestry");
|
|
118
|
+
}
|
|
119
|
+
for (const boundary of view.lineage.slice(1)) {
|
|
120
|
+
if (!SCOPES.some((scope) => view.scopes[scope].patches.some((record) => record.transition.id === boundary.id))) {
|
|
121
|
+
throw new Error("Temporal boundary has no semantic patch");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Adopt revision-proven inherited streams without rewriting their checkpoints or tails. */
|
|
126
|
+
export function adoptTemporalStreams(scopes, id) {
|
|
127
|
+
const boundaries = Object.values(scopes).flatMap((stream) => [stream.checkpoint.through, ...stream.patches.map((record) => record.transition)]);
|
|
128
|
+
const origin = { id, position: Math.max(...boundaries.map((boundary) => boundary.position)) + 1, parent: null };
|
|
129
|
+
const view = { lineage: [origin], scopes: structuredClone(scopes) };
|
|
130
|
+
validateTemporalState(view);
|
|
131
|
+
return view;
|
|
132
|
+
}
|
|
133
|
+
/** New or migrated state starts at a proven current boundary, with no invented past. */
|
|
134
|
+
export function createTemporalState(states, id) {
|
|
135
|
+
const through = { id, position: 0, parent: null };
|
|
136
|
+
const stream = (scope) => ({
|
|
137
|
+
checkpoint: { through: structuredClone(through), state: structuredClone(states[scope]) },
|
|
138
|
+
patches: [],
|
|
139
|
+
});
|
|
140
|
+
const view = { lineage: [through], scopes: { global: stream("global"), cwd: stream("cwd"), session: stream("session") } };
|
|
141
|
+
validateTemporalState(view);
|
|
142
|
+
return view;
|
|
143
|
+
}
|
|
144
|
+
function scopeAt(stream, boundary) {
|
|
145
|
+
let state = structuredClone(stream.checkpoint.state);
|
|
146
|
+
for (const record of stream.patches) {
|
|
147
|
+
if (record.transition.position > boundary.position)
|
|
148
|
+
break;
|
|
149
|
+
state = apply(state, record.patch);
|
|
150
|
+
}
|
|
151
|
+
return state;
|
|
152
|
+
}
|
|
153
|
+
/** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
|
|
154
|
+
export function readTemporalState(view, offset = 0, scope) {
|
|
155
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > RECENT_TRANSITION_LIMIT) {
|
|
156
|
+
throw new Error("State Flow hot-history offset must be an integer from 0 to 7");
|
|
157
|
+
}
|
|
158
|
+
if (scope !== undefined && !SCOPES.includes(scope))
|
|
159
|
+
throw new Error("Unknown temporal scope");
|
|
160
|
+
validateTemporalState(view);
|
|
161
|
+
const boundary = view.lineage[view.lineage.length - 1 - offset];
|
|
162
|
+
if (!boundary)
|
|
163
|
+
throw new Error("Requested history predates the proven temporal origin");
|
|
164
|
+
if (scope !== undefined)
|
|
165
|
+
return scopeAt(view.scopes[scope], boundary);
|
|
166
|
+
return overlayStates(...SCOPES.map((owner) => scopeAt(view.scopes[owner], boundary)));
|
|
167
|
+
}
|
|
168
|
+
/** Allocate the identity outside this algebra; only materially effective patches accept it. */
|
|
169
|
+
export function advanceTemporalState(view, transitions, id) {
|
|
170
|
+
validateTemporalState(view);
|
|
171
|
+
if (transitions.length === 0)
|
|
172
|
+
return view;
|
|
173
|
+
validateRecentTransition({ id, at: 0, transitions });
|
|
174
|
+
const head = view.lineage.at(-1);
|
|
175
|
+
const changes = transitions.filter(({ scope, patch }) => {
|
|
176
|
+
const current = scopeAt(view.scopes[scope], head);
|
|
177
|
+
return !sameJson(current, apply(current, patch));
|
|
178
|
+
});
|
|
179
|
+
if (changes.length === 0)
|
|
180
|
+
return view;
|
|
181
|
+
const knownIds = new Set(view.lineage.map((boundary) => boundary.id));
|
|
182
|
+
for (const scope of SCOPES) {
|
|
183
|
+
knownIds.add(view.scopes[scope].checkpoint.through.id);
|
|
184
|
+
for (const record of view.scopes[scope].patches)
|
|
185
|
+
knownIds.add(record.transition.id);
|
|
186
|
+
}
|
|
187
|
+
if (knownIds.has(id))
|
|
188
|
+
throw new Error("State Flow transition identity has already been used");
|
|
189
|
+
const boundary = { id, position: head.position + 1, parent: head.id };
|
|
190
|
+
validateBoundary(boundary);
|
|
191
|
+
const next = structuredClone(view);
|
|
192
|
+
for (const { scope, patch } of changes) {
|
|
193
|
+
const stream = next.scopes[scope];
|
|
194
|
+
if (stream.patches.length === RECENT_TRANSITION_LIMIT) {
|
|
195
|
+
const folded = stream.patches.shift();
|
|
196
|
+
stream.checkpoint = { through: folded.transition, state: apply(stream.checkpoint.state, folded.patch) };
|
|
197
|
+
}
|
|
198
|
+
stream.patches.push({ transition: structuredClone(boundary), patch: structuredClone(patch) });
|
|
199
|
+
}
|
|
200
|
+
next.lineage = [...next.lineage, boundary].slice(-(RECENT_TRANSITION_LIMIT + 1));
|
|
201
|
+
validateTemporalState(next);
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ArtifactProvenance } from "./artifact.ts";
|
|
2
|
+
import type { SuccessfulArtifactRead } from "./acquisition.ts";
|
|
3
|
+
import { type AcceptedTransition } from "./history.ts";
|
|
4
|
+
import { type SuccessfulSkillRead } from "./skills.ts";
|
|
5
|
+
import type { Snapshot } from "./snapshot.ts";
|
|
6
|
+
import type { AtomicScopePatches, ScopedStates, StateScope, TerminalTransition } from "./state.ts";
|
|
7
|
+
export interface StagedScopedTransition {
|
|
8
|
+
nextStates: ScopedStates;
|
|
9
|
+
stateHashes: Record<StateScope, string>;
|
|
10
|
+
/** Fresh runtime-owned provenance for artifacts compiled in this transition. */
|
|
11
|
+
provenanceUpdates: Record<StateScope, Record<string, ArtifactProvenance>>;
|
|
12
|
+
causalBasis: string;
|
|
13
|
+
committed: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Validate that final eligibility has no pending acquisition/compilation obligation. */
|
|
16
|
+
export declare function validateFinalEligibility(currentStates: ScopedStates, successfulSkillReads: Iterable<SuccessfulSkillRead>, causalBasis: string, successfulArtifactReads?: Iterable<SuccessfulArtifactRead>): void;
|
|
17
|
+
/** Stage one canonical atomic scope cohort without changing the finalized response. */
|
|
18
|
+
export declare function stageAtomicScopePatches(currentStates: ScopedStates, patches: AtomicScopePatches, successfulSkillReads: Iterable<SuccessfulSkillRead>, causalBasis: string, successfulArtifactReads?: Iterable<SuccessfulArtifactRead>): StagedScopedTransition;
|
|
19
|
+
export declare function stageScopedTransition(currentStates: ScopedStates, transition: TerminalTransition, successfulSkillReads: Iterable<SuccessfulSkillRead>, causalBasis: string, successfulArtifactReads?: Iterable<SuccessfulArtifactRead>): StagedScopedTransition;
|
|
20
|
+
/** Commit one accepted transition; durable publication receives all changed scopes as one cohort. */
|
|
21
|
+
export interface CommitScopedTransitionOptions {
|
|
22
|
+
/** Runtime response reconciliation finalizes bootstrap lifecycle state. */
|
|
23
|
+
finalizeRun?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function commitScopedTransition(snapshot: Snapshot, states: ScopedStates, stage: StagedScopedTransition, publishDurable: (accepted: AcceptedTransition | undefined, nextSnapshot: Snapshot) => void, causalBasis: string, options?: CommitScopedTransitionOptions): boolean;
|