@llblab/pi-kit 0.18.2 → 0.19.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 +11 -0
- package/README.md +3 -1
- 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 +23 -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 +160 -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 +155 -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/package.json +2 -2
- 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,19 +1,41 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { projectArtifactForModel, type ArtifactInvalidationNotice } from "./artifact.ts";
|
|
2
|
+
import { projectArtifactForModel, type ArtifactInvalidationNotice, type ArtifactModelHints } from "./artifact.ts";
|
|
3
3
|
import type { RecentTransitionWindow } from "./history.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { isObject, presentationJson, type JsonValue } from "./json.ts";
|
|
5
5
|
import type { Snapshot } from "./snapshot.ts";
|
|
6
6
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
7
|
-
import { projectStateForModel, type MaterializedState } from "./state.ts";
|
|
7
|
+
import { projectStateForModel, type MaterializedState, type ModelState } from "./state.ts";
|
|
8
|
+
|
|
9
|
+
/** Refresh only our section; Pi owns system frames, tools and forced-prompt precedence. */
|
|
10
|
+
export function projectSystemProtocol(messages: AgentMessage[], protocol: string | undefined): AgentMessage[] {
|
|
11
|
+
let lastSystem = -1;
|
|
12
|
+
let current: string | undefined;
|
|
13
|
+
for (let index = 0; index < messages.length; index++) {
|
|
14
|
+
const message = messages[index]!;
|
|
15
|
+
if (message.role !== "system") continue;
|
|
16
|
+
lastSystem = index;
|
|
17
|
+
if (message.sections && Object.hasOwn(message.sections, "state_flow")) current = message.sections.state_flow ?? undefined;
|
|
18
|
+
}
|
|
19
|
+
const section = protocol === undefined ? undefined : `<state_flow>\n${protocol}\n</state_flow>`;
|
|
20
|
+
if (lastSystem < 0 || current === section) return messages;
|
|
21
|
+
return messages.map((message, index) => {
|
|
22
|
+
if (message.role !== "system") return message;
|
|
23
|
+
const ownsSection = message.sections !== undefined && Object.hasOwn(message.sections, "state_flow");
|
|
24
|
+
if (!ownsSection && (index !== lastSystem || section === undefined)) return message;
|
|
25
|
+
const sections = { ...message.sections };
|
|
26
|
+
delete sections.state_flow;
|
|
27
|
+
if (index === lastSystem && section !== undefined) sections.state_flow = section;
|
|
28
|
+
return { ...message, sections };
|
|
29
|
+
});
|
|
30
|
+
}
|
|
8
31
|
|
|
9
|
-
export const VALIDATION_MESSAGE_TYPE = "state-flow-validation";
|
|
10
32
|
const LAZY_HINT_PATH = "effective.lazy";
|
|
11
33
|
const LAZY_HINT_MAX_KEYS = 32;
|
|
12
34
|
const LAZY_HINT_MAX_JSON_CHARS = 1024;
|
|
13
35
|
|
|
14
36
|
type LazyValueKind = "array" | "boolean" | "null" | "number" | "object" | "string";
|
|
15
37
|
|
|
16
|
-
function lazyValueKind(value:
|
|
38
|
+
function lazyValueKind(value: JsonValue): LazyValueKind {
|
|
17
39
|
if (value === null) return "null";
|
|
18
40
|
if (Array.isArray(value)) return "array";
|
|
19
41
|
if (typeof value === "object") return "object";
|
|
@@ -22,9 +44,9 @@ function lazyValueKind(value: Exclude<MaterializedState["lazy"], undefined>): La
|
|
|
22
44
|
|
|
23
45
|
/** Fixed-budget navigation only: never place lazy bodies or partial key catalogs in baseline context. */
|
|
24
46
|
export function lazyNavigationHint(state: MaterializedState): { available: boolean; path: string; keys?: Record<string, LazyValueKind> } {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
const entries = isObject(state.lazy) ? Object.entries(state.lazy) : [];
|
|
48
|
+
const base = { available: entries.length > 0, path: LAZY_HINT_PATH };
|
|
49
|
+
if (!base.available) return base;
|
|
28
50
|
if (entries.length > LAZY_HINT_MAX_KEYS) return base;
|
|
29
51
|
const keys = Object.fromEntries(entries.map(([key, value]) => [key, lazyValueKind(value)]));
|
|
30
52
|
return JSON.stringify(keys).length <= LAZY_HINT_MAX_JSON_CHARS ? { ...base, keys } : base;
|
|
@@ -56,29 +78,25 @@ function messageText(message: AgentMessage): string {
|
|
|
56
78
|
return contentText((message as { content?: unknown }).content);
|
|
57
79
|
}
|
|
58
80
|
|
|
59
|
-
export function
|
|
60
|
-
return messages.filter((message) => {
|
|
61
|
-
return !(message.role === "custom" && message.customType === VALIDATION_MESSAGE_TYPE);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function createPassiveContinuation(state: MaterializedState, startedAt = Date.now(), activeRunStartedAt?: number): PassiveContinuation {
|
|
81
|
+
export function createPassiveContinuation(state: ModelState, startedAt = Date.now(), activeRunStartedAt?: number): PassiveContinuation {
|
|
66
82
|
return {
|
|
67
83
|
startedAt,
|
|
68
84
|
...(activeRunStartedAt === undefined ? {} : { activeRunStartedAt }),
|
|
69
|
-
handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${
|
|
85
|
+
handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${presentationJson({ state, continuation: "State Flow semantics are disabled; this handoff replaces completed history while retaining the active and post-stop trajectory." })}`),
|
|
70
86
|
};
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
/** Keep the interrupted run through later results; an idle stop retains only later conversation. */
|
|
74
90
|
export function passiveContinuationMessages(messages: AgentMessage[], continuation: PassiveContinuation): AgentMessage[] {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
if (continuation.activeRunStartedAt !== undefined) {
|
|
92
|
+
const trajectory = currentRunTrajectory(messages, "", continuation.activeRunStartedAt);
|
|
93
|
+
return [continuation.handoff, ...trajectory.messages];
|
|
94
|
+
}
|
|
95
|
+
const start = messages.findIndex((message) => message.role === "user"
|
|
78
96
|
&& typeof message.timestamp === "number"
|
|
79
97
|
&& message.timestamp >= continuation.startedAt);
|
|
80
98
|
return [continuation.handoff, ...messages.filter((message, index) =>
|
|
81
|
-
message.role === "custom"
|
|
99
|
+
message.role === "custom" || (start >= 0 && index >= start))];
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
function projectRecentForModel(recent: RecentTransitionWindow): RecentTransitionWindow {
|
|
@@ -100,61 +118,36 @@ export function runtimeContextMessage(
|
|
|
100
118
|
recentTransitions: RecentTransitionWindow = [],
|
|
101
119
|
artifactInvalidations: readonly ArtifactInvalidationNotice[] = [],
|
|
102
120
|
rehydrationPhase?: RehydrationPhase,
|
|
103
|
-
|
|
121
|
+
artifactHints: ArtifactModelHints = {},
|
|
104
122
|
): AgentMessage {
|
|
105
|
-
if (snapshot.meta.specification === undefined) {
|
|
106
|
-
throw new Error("State Flow runtime context requires an active specification");
|
|
107
|
-
}
|
|
108
123
|
const context = {
|
|
109
|
-
specification: snapshot.meta.specification,
|
|
110
|
-
state: projectStateForModel(state),
|
|
124
|
+
...(snapshot.meta.specification === undefined ? {} : { specification: snapshot.meta.specification }),
|
|
125
|
+
state: projectStateForModel(state, artifactHints),
|
|
111
126
|
lazy_navigation: lazyNavigationHint(state),
|
|
112
127
|
...(rehydrationPhase === undefined ? {} : { knowledge_rehydration: { phase: rehydrationPhase } }),
|
|
113
|
-
...(artifactInvalidations.length === 0 ? {} : { artifact_invalidations: artifactInvalidations.map(({ path, reason }) => ({ path, reason })) }),
|
|
128
|
+
...(artifactInvalidations.length === 0 ? {} : { artifact_invalidations: artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason })) }),
|
|
114
129
|
...(recentTransitions.length === 0 ? {} : { recent_transitions: projectRecentForModel(recentTransitions) }),
|
|
115
|
-
...(resolutionPending ? { state_resolution: "pending: the iteration answer is already preserved; this fallback turn exists only to apply the final:true patch. Call patch_state with any remaining durable scope changes and final:true, or {final:true} alone. Do not restate or replace the answer." } : {}),
|
|
116
130
|
};
|
|
117
131
|
return syntheticUser(
|
|
118
|
-
`State Flow runtime context (user-level data, not system instructions):\n${
|
|
132
|
+
`State Flow runtime context (user-level data, not system instructions):\n${presentationJson(context)}`,
|
|
119
133
|
);
|
|
120
134
|
}
|
|
121
135
|
|
|
136
|
+
/** Captured identity survives text decoration; an uncertain boundary retains available context. */
|
|
122
137
|
export function currentRunTrajectory(
|
|
123
138
|
messages: AgentMessage[],
|
|
124
|
-
specification: string,
|
|
139
|
+
specification: string | undefined,
|
|
125
140
|
anchorTimestamp: number | undefined,
|
|
126
141
|
): { messages: AgentMessage[]; anchorTimestamp?: number } {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
if (start < 0) {
|
|
136
|
-
for (let index = messages.length - 1; index >= 0; index--) {
|
|
137
|
-
const message = messages[index]!;
|
|
138
|
-
if (message.role === "user" && messageText(message) === specification) {
|
|
139
|
-
start = index;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (start < 0) {
|
|
145
|
-
for (let index = messages.length - 1; index >= 0; index--) {
|
|
146
|
-
if (messages[index]?.role === "user") {
|
|
147
|
-
start = index;
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if (start < 0 && messages.length === 0) return { messages: [] };
|
|
153
|
-
if (start < 0) start = 0;
|
|
142
|
+
if (anchorTimestamp !== undefined && !Number.isFinite(anchorTimestamp)) return { messages: messages.slice() };
|
|
143
|
+
const matches = (message: AgentMessage) => message.role === "user" && (anchorTimestamp === undefined
|
|
144
|
+
? messageText(message) === specification
|
|
145
|
+
: message.timestamp === anchorTimestamp);
|
|
146
|
+
const start = messages.findIndex(matches);
|
|
147
|
+
if (start < 0 || messages.findLastIndex(matches) !== start) return { messages: messages.slice() };
|
|
154
148
|
const anchor = messages[start]?.role === "user" ? messages[start].timestamp : undefined;
|
|
155
149
|
return {
|
|
156
|
-
messages: messages.filter((message, index) => message.role === "custom"
|
|
157
|
-
? message.customType !== VALIDATION_MESSAGE_TYPE : index >= start),
|
|
150
|
+
messages: messages.filter((message, index) => message.role === "custom" || index >= start),
|
|
158
151
|
...(typeof anchor === "number" ? { anchorTimestamp: anchor } : {}),
|
|
159
152
|
};
|
|
160
153
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
-
import { join,
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
4
3
|
import { loadScopeStream, resolveSessionAddress, sessionRuntimePaths } from "./durable.ts";
|
|
5
|
-
import {
|
|
4
|
+
import { MAX_HISTORY_LIMIT } from "./history.ts";
|
|
6
5
|
import { parseSessionRuntime } from "./snapshot.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { validateScopeLineage } from "./temporal.ts";
|
|
8
7
|
|
|
9
8
|
export type ContinuationTransport = "local" | "sdk" | "telegram" | string;
|
|
10
9
|
|
|
@@ -228,24 +227,16 @@ export function inspectStateFlowContinuationProvenance(
|
|
|
228
227
|
if (config === undefined && runtimeSource === undefined && meta === undefined) {
|
|
229
228
|
return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
230
229
|
}
|
|
231
|
-
const runtime = parseSessionRuntime(config, runtimeSource, header.cwd, header.id
|
|
230
|
+
const runtime = parseSessionRuntime(config, runtimeSource, header.cwd, header.id);
|
|
232
231
|
if (!runtime) return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
233
232
|
if (!runtime.config.enabled) return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow stopped on selected runtime" };
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
const runtimePath = relative(resolve(repositoryRoot), runtimeSource === undefined ? paths.meta : paths.runtime);
|
|
243
|
-
if (runtimePath.startsWith("..")) throw new Error("runtime provenance escapes repository");
|
|
244
|
-
const owner = execFileSync("git", ["-C", repositoryRoot, "log", "-1", "--format=%H", "--", runtimePath], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
245
|
-
if (!owner) throw new Error("runtime owner commit is unavailable");
|
|
246
|
-
const restored = loadTemporalRevision(header.cwd, header.id, repositoryRoot, owner, sessionKey);
|
|
247
|
-
if (!restored.runtime || !restored.runtime.document.config.enabled) throw new Error("selected Git runtime is not enabled");
|
|
248
|
-
return { stateFlow: { enabled: true, restorable: true }, reason: `exact Git runtime ${owner.slice(0, 12)} is restorable` };
|
|
233
|
+
const global = loadScopeStream(header.cwd, header.id, "global", repositoryRoot, sessionKey);
|
|
234
|
+
const cwd = loadScopeStream(header.cwd, header.id, "cwd", repositoryRoot, sessionKey);
|
|
235
|
+
const session = loadScopeStream(header.cwd, header.id, "session", repositoryRoot, sessionKey);
|
|
236
|
+
if (!global || !cwd || !session) throw new Error("incomplete canonical temporal cohort");
|
|
237
|
+
// Shared streams are current, independently validated by their codecs, not frozen to this session's history.
|
|
238
|
+
validateScopeLineage(session, "session", runtime.meta.lineage, MAX_HISTORY_LIMIT);
|
|
239
|
+
return { stateFlow: { enabled: true, restorable: true }, reason: "canonical session lineage is valid beside current shared streams" };
|
|
249
240
|
} catch (error) {
|
|
250
241
|
return { stateFlow: { enabled: true, restorable: false }, reason: `State Flow runtime is ineligible: ${error instanceof Error ? error.message : String(error)}` };
|
|
251
242
|
}
|
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
serializeArtifactProvenanceRegistry,
|
|
19
19
|
type ArtifactProvenanceRegistry,
|
|
20
20
|
} from "./artifact.ts";
|
|
21
|
+
import { MAX_HISTORY_LIMIT } from "./history.ts";
|
|
21
22
|
import { canonicalJson, isJsonValue, isObject } from "./json.ts";
|
|
22
23
|
import { validateScopeStream, validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
23
|
-
import {
|
|
24
|
+
import type { StateScope } from "./state.ts";
|
|
24
25
|
|
|
25
26
|
const SESSION_KEY_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
26
27
|
const STATE_FILE = "state.json";
|
|
@@ -36,6 +37,18 @@ export interface ScopeStreamSources {
|
|
|
36
37
|
temporal: ScopeTemporalMetadata;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** Read-only activation eligibility for one canonical CWD scope. */
|
|
41
|
+
export function hasCwdMaterialization(cwd: string, repositoryRoot: string): boolean {
|
|
42
|
+
const root = resolve(repositoryRoot);
|
|
43
|
+
const directory = cwdScopePaths(cwd, root).directory;
|
|
44
|
+
const [checkpoint, patches, meta] = captureOwnedFileBases([
|
|
45
|
+
join(directory, CHECKPOINT_FILE), join(directory, PATCHES_FILE), join(directory, META_FILE),
|
|
46
|
+
], root);
|
|
47
|
+
if (checkpoint!.content !== undefined) return parseScopeStream(checkpoint!.content, patches!.content, "cwd", cwd, meta!.content) !== undefined;
|
|
48
|
+
if (patches!.content !== undefined) throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
export interface ScopeTemporalMetadata {
|
|
40
53
|
checkpoint: ScopeStream["checkpoint"]["through"];
|
|
41
54
|
patches: ScopeStream["patches"][number]["transition"][];
|
|
@@ -48,7 +61,7 @@ export interface SessionAddress {
|
|
|
48
61
|
|
|
49
62
|
/** Semantic files contain no runtime envelope; temporal boundaries and CWD ownership live in meta.json. */
|
|
50
63
|
export function serializeScopeStream(stream: ScopeStream, scope: StateScope, cwdIdentity?: string): ScopeStreamSources {
|
|
51
|
-
validateScopeStream(stream, scope);
|
|
64
|
+
validateScopeStream(stream, scope, MAX_HISTORY_LIMIT);
|
|
52
65
|
if (scope === "cwd" && cwdIdentity === undefined) throw new Error("State Flow CWD scope serialization requires its canonical identity");
|
|
53
66
|
if (scope !== "cwd" && cwdIdentity !== undefined) throw new Error("Only State Flow CWD scope serialization accepts a CWD identity");
|
|
54
67
|
return {
|
|
@@ -73,6 +86,7 @@ export function classifyScopeStream(
|
|
|
73
86
|
expectedCwd?: string,
|
|
74
87
|
metaSource?: string,
|
|
75
88
|
): ScopeStreamPresence {
|
|
89
|
+
if (scope !== "global" && scope !== "cwd" && scope !== "session") throw new Error("Unknown temporal scope");
|
|
76
90
|
if (checkpointSource === undefined && patchesSource === undefined) return { kind: "absent" };
|
|
77
91
|
if (checkpointSource === undefined || patchesSource === undefined) {
|
|
78
92
|
throw new Error(`State Flow ${scope} scope has an incomplete checkpoint/tail pair`);
|
|
@@ -99,40 +113,21 @@ export function classifyScopeStream(
|
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
115
|
const temporal = meta?.temporal;
|
|
102
|
-
if (isObject(temporal)
|
|
103
|
-
|
|
104
|
-
const owner = meta?.owner;
|
|
105
|
-
if (scope === "cwd" && expectedCwd !== undefined
|
|
106
|
-
&& (!isObject(owner) || Object.keys(owner).join(",") !== "cwd" || owner.cwd !== resolve(expectedCwd))) {
|
|
107
|
-
throw new Error(owner === undefined ? "State Flow CWD scope identity is missing" : "State Flow CWD scope identity mismatch");
|
|
108
|
-
}
|
|
109
|
-
const boundaries = temporal.patches;
|
|
110
|
-
if (boundaries.length !== patches.length) throw new Error(`State Flow ${scope} temporal metadata does not match its semantic tail`);
|
|
111
|
-
const stream = {
|
|
112
|
-
checkpoint: { through: temporal.checkpoint, state: checkpoint },
|
|
113
|
-
patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
|
|
114
|
-
};
|
|
115
|
-
validateScopeStream(stream, scope);
|
|
116
|
-
return { kind: "present", stream };
|
|
117
|
-
}
|
|
118
|
-
// Complete predecessor envelopes are the only no-meta form that may be unwrapped.
|
|
119
|
-
let legacyCheckpoint = checkpoint;
|
|
120
|
-
if (isObject(checkpoint) && Object.hasOwn(checkpoint, "owner")) {
|
|
121
|
-
const { owner, ...semantic } = checkpoint;
|
|
122
|
-
if (scope !== "cwd" || !isObject(owner) || Object.keys(owner).join(",") !== "cwd" || typeof owner.cwd !== "string") {
|
|
123
|
-
throw new Error("Invalid State Flow CWD scope identity");
|
|
124
|
-
}
|
|
125
|
-
if (expectedCwd !== undefined && owner.cwd !== resolve(expectedCwd)) throw new Error("State Flow CWD scope identity mismatch");
|
|
126
|
-
legacyCheckpoint = semantic;
|
|
127
|
-
} else if (scope === "cwd" && expectedCwd !== undefined) {
|
|
128
|
-
throw new Error("State Flow CWD scope identity is missing");
|
|
116
|
+
if (!isObject(temporal) || !Object.hasOwn(temporal, "checkpoint") || !Array.isArray(temporal.patches)) {
|
|
117
|
+
throw new Error(`Unsupported State Flow ${scope} storage format`);
|
|
129
118
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
119
|
+
const owner = meta?.owner;
|
|
120
|
+
if (scope === "cwd" && expectedCwd !== undefined
|
|
121
|
+
&& (!isObject(owner) || Object.keys(owner).join(",") !== "cwd" || owner.cwd !== resolve(expectedCwd))) {
|
|
122
|
+
throw new Error(owner === undefined ? "State Flow CWD scope identity is missing" : "State Flow CWD scope identity mismatch");
|
|
133
123
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
124
|
+
const boundaries = temporal.patches;
|
|
125
|
+
if (boundaries.length !== patches.length) throw new Error(`State Flow ${scope} temporal metadata does not match its semantic tail`);
|
|
126
|
+
const stream = {
|
|
127
|
+
checkpoint: { through: temporal.checkpoint, state: checkpoint },
|
|
128
|
+
patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
|
|
129
|
+
};
|
|
130
|
+
validateScopeStream(stream, scope, MAX_HISTORY_LIMIT);
|
|
136
131
|
return { kind: "present", stream };
|
|
137
132
|
}
|
|
138
133
|
|
|
@@ -174,17 +169,17 @@ export function sessionRuntimePaths(cwd: string, sessionId: string, repositoryRo
|
|
|
174
169
|
export function loadScopeStream(cwd: string, sessionId: string, scope: StateScope, repositoryRoot: string, sessionKey = sessionId): ScopeStream | undefined {
|
|
175
170
|
const paths = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
|
|
176
171
|
if (readRegularBytes(join(paths.directory, STATE_FILE), repositoryRoot) !== undefined) {
|
|
177
|
-
throw new Error(`
|
|
172
|
+
throw new Error(`Unsupported State Flow storage format: ${paths.directory}`);
|
|
178
173
|
}
|
|
179
174
|
return parseScopeStream(readRegularFile(paths.checkpoint, repositoryRoot), readRegularFile(paths.patches, repositoryRoot), scope, scope === "cwd" ? cwd : undefined, readRegularFile(paths.meta, repositoryRoot));
|
|
180
175
|
}
|
|
181
176
|
|
|
182
|
-
/** Include
|
|
177
|
+
/** Include unsupported predecessor names in the CAS basis so they cannot race canonical publication. */
|
|
183
178
|
export function captureTemporalFileBases(cwd: string, sessionId: string, repositoryRoot: string, sessionKey = sessionId): DurableFileBase[] {
|
|
184
179
|
const paths = (["global", "cwd", "session"] as const).flatMap((scope) => {
|
|
185
180
|
const pair = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
|
|
186
181
|
const runtime = scope === "session" ? sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey) : undefined;
|
|
187
|
-
return [pair.checkpoint, pair.patches,
|
|
182
|
+
return [pair.checkpoint, pair.patches, ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.runtime, runtime.meta])];
|
|
188
183
|
});
|
|
189
184
|
return captureOwnedFileBases(paths, repositoryRoot);
|
|
190
185
|
}
|
|
@@ -198,26 +193,20 @@ export function temporalStateFileUpdates(
|
|
|
198
193
|
repositoryRoot: string,
|
|
199
194
|
sessionKey = sessionId,
|
|
200
195
|
): OwnedFileUpdate[] {
|
|
201
|
-
validateTemporalState(view);
|
|
196
|
+
validateTemporalState(view, MAX_HISTORY_LIMIT);
|
|
202
197
|
const seen = new Set<StateScope>();
|
|
203
198
|
return scopes.flatMap((scope) => {
|
|
204
199
|
if (seen.has(scope)) throw new Error(`Duplicate temporal scope update: ${scope}`);
|
|
205
200
|
seen.add(scope);
|
|
206
201
|
const paths = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
|
|
207
202
|
if (readRegularBytes(join(paths.directory, STATE_FILE), repositoryRoot) !== undefined) {
|
|
208
|
-
throw new Error(`
|
|
203
|
+
throw new Error(`Unsupported State Flow storage format: ${paths.directory}`);
|
|
209
204
|
}
|
|
210
205
|
const sources = serializeScopeStream(view.scopes[scope], scope, scope === "cwd" ? cwd : undefined);
|
|
211
206
|
return [{ path: paths.checkpoint, content: sources.checkpoint }, { path: paths.patches, content: sources.patches }];
|
|
212
207
|
});
|
|
213
208
|
}
|
|
214
209
|
|
|
215
|
-
export interface DurablePaths {
|
|
216
|
-
repositoryRoot: string;
|
|
217
|
-
globalState: string;
|
|
218
|
-
globalPatches: string;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
210
|
/** Merge authoritative owned leaves while preserving forward-compatible metadata siblings. */
|
|
222
211
|
export function serializeScopeMetadata(
|
|
223
212
|
registry: Readonly<ArtifactProvenanceRegistry> | undefined, stream: ScopeStream, scope: StateScope,
|
|
@@ -225,7 +214,7 @@ export function serializeScopeMetadata(
|
|
|
225
214
|
): string {
|
|
226
215
|
const existing = parseMetadataDocument(existingSource, `State Flow metadata`);
|
|
227
216
|
if (scope === "session") {
|
|
228
|
-
for (const key of ["identity", "lineage", "step", "specification", "validation", "bootstrap"
|
|
217
|
+
for (const key of ["identity", "lineage", "step", "specification", "validation", "bootstrap"]) delete existing[key];
|
|
229
218
|
}
|
|
230
219
|
const sources = serializeScopeStream(stream, scope, cwdIdentity);
|
|
231
220
|
const value = {
|
|
@@ -263,15 +252,13 @@ export function parseScopeProvenance(source: string | undefined, path: string):
|
|
|
263
252
|
|
|
264
253
|
export interface ScopePaths {
|
|
265
254
|
directory: string;
|
|
266
|
-
state: string;
|
|
267
|
-
patches: string;
|
|
268
255
|
}
|
|
269
256
|
|
|
270
257
|
export interface DurableFileBase {
|
|
271
258
|
path: string;
|
|
272
259
|
identity: "missing" | `sha256:${string}`;
|
|
273
260
|
content?: string;
|
|
274
|
-
/** Opaque originals for byte-exact rollback
|
|
261
|
+
/** Opaque originals for byte-exact rollback. */
|
|
275
262
|
bytes?: Uint8Array;
|
|
276
263
|
}
|
|
277
264
|
|
|
@@ -280,16 +267,6 @@ export function getDurableRepositoryRoot(agentDir = getAgentDir()): string {
|
|
|
280
267
|
return resolve(agentDir, "state-flow");
|
|
281
268
|
}
|
|
282
269
|
|
|
283
|
-
/** Legacy snapshot paths retained only for one-way migration and exact ownership checks. */
|
|
284
|
-
export function durablePaths(repositoryRoot = getDurableRepositoryRoot()): DurablePaths {
|
|
285
|
-
const root = resolve(repositoryRoot);
|
|
286
|
-
return {
|
|
287
|
-
repositoryRoot: root,
|
|
288
|
-
globalState: join(root, STATE_FILE),
|
|
289
|
-
globalPatches: join(root, PATCHES_FILE),
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
270
|
/** Match Pi's native project-session directory convention exactly. */
|
|
294
271
|
export function cwdScopeKey(cwd: string): string {
|
|
295
272
|
const canonical = resolve(cwd);
|
|
@@ -319,8 +296,7 @@ export function resolveSessionAddress(sessionFile: string | undefined, sessionId
|
|
|
319
296
|
}
|
|
320
297
|
|
|
321
298
|
export function cwdScopePaths(cwd: string, repositoryRoot = getDurableRepositoryRoot()): ScopePaths {
|
|
322
|
-
|
|
323
|
-
return { directory, state: join(directory, STATE_FILE), patches: join(directory, PATCHES_FILE) };
|
|
299
|
+
return { directory: join(resolve(repositoryRoot), cwdScopeKey(cwd)) };
|
|
324
300
|
}
|
|
325
301
|
|
|
326
302
|
export function sessionScopePaths(
|
|
@@ -329,45 +305,27 @@ export function sessionScopePaths(
|
|
|
329
305
|
repositoryRoot = getDurableRepositoryRoot(),
|
|
330
306
|
sessionKey = sessionId,
|
|
331
307
|
): ScopePaths {
|
|
332
|
-
|
|
333
|
-
return { directory, state: join(directory, STATE_FILE), patches: join(directory, PATCHES_FILE) };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function cwdStatePath(cwd: string, repositoryRoot = getDurableRepositoryRoot()): string {
|
|
337
|
-
return cwdScopePaths(cwd, repositoryRoot).state;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function cwdPatchesPath(cwd: string, repositoryRoot = getDurableRepositoryRoot()): string {
|
|
341
|
-
return cwdScopePaths(cwd, repositoryRoot).patches;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export function sessionStatePath(cwd: string, sessionId: string, repositoryRoot = getDurableRepositoryRoot(), sessionKey = sessionId): string {
|
|
345
|
-
return sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).state;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
export function sessionPatchesPath(cwd: string, sessionId: string, repositoryRoot = getDurableRepositoryRoot(), sessionKey = sessionId): string {
|
|
349
|
-
return sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).patches;
|
|
308
|
+
return { directory: join(cwdScopePaths(cwd, repositoryRoot).directory, sessionScopeKey(sessionKey)) };
|
|
350
309
|
}
|
|
351
310
|
|
|
352
|
-
/** Exact semantic file shapes
|
|
311
|
+
/** Exact canonical semantic and runtime file shapes. */
|
|
353
312
|
export function isStateFlowOwnedPath(candidate: string, repositoryRoot = getDurableRepositoryRoot()): boolean {
|
|
354
313
|
const root = resolve(repositoryRoot);
|
|
355
314
|
const absolute = resolve(candidate);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|| absolute === join(root, CHECKPOINT_FILE) || absolute === join(root, META_FILE)) return true;
|
|
315
|
+
if (absolute === join(root, CHECKPOINT_FILE) || absolute === join(root, PATCHES_FILE)
|
|
316
|
+
|| absolute === join(root, META_FILE)) return true;
|
|
359
317
|
const segments = relative(root, absolute).split(sep);
|
|
360
318
|
const cwdKey = (value: string) => value.startsWith("--") && value.endsWith("--");
|
|
361
319
|
const sessionKey = (value: string) => {
|
|
362
320
|
try { return sessionScopeKey(value) === value; } catch { return false; }
|
|
363
321
|
};
|
|
364
322
|
if (segments.length === 2) {
|
|
365
|
-
return cwdKey(segments[0]!) && (segments[1] ===
|
|
323
|
+
return cwdKey(segments[0]!) && (segments[1] === CHECKPOINT_FILE || segments[1] === PATCHES_FILE || segments[1] === META_FILE);
|
|
366
324
|
}
|
|
367
325
|
if (segments.length === 3) {
|
|
368
326
|
return cwdKey(segments[0]!)
|
|
369
327
|
&& sessionKey(segments[1]!)
|
|
370
|
-
&& (segments[2] ===
|
|
328
|
+
&& (segments[2] === CHECKPOINT_FILE || segments[2] === PATCHES_FILE
|
|
371
329
|
|| segments[2] === "config.json" || segments[2] === RUNTIME_FILE || segments[2] === META_FILE);
|
|
372
330
|
}
|
|
373
331
|
return false;
|
|
@@ -457,7 +415,7 @@ function assertCurrentBytes(path: string, root: string, expected: Uint8Array | u
|
|
|
457
415
|
}
|
|
458
416
|
}
|
|
459
417
|
|
|
460
|
-
/** Capture exact owned bytes for one compare-and-swap publication
|
|
418
|
+
/** Capture exact owned bytes for one compare-and-swap publication cohort. */
|
|
461
419
|
export function captureOwnedFileBases(paths: readonly string[], repositoryRoot: string): DurableFileBase[] {
|
|
462
420
|
const root = resolve(repositoryRoot);
|
|
463
421
|
return paths.map((path) => {
|