@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
|
@@ -11,6 +11,8 @@ export interface StateFlowConfig {
|
|
|
11
11
|
autoStart: boolean;
|
|
12
12
|
/** Opt-in local capture of rejected patch attempts and unresolved terminal drafts. */
|
|
13
13
|
logging: boolean;
|
|
14
|
+
/** Show successful patch_state arguments in the interactive tool row. */
|
|
15
|
+
showSuccessfulPatches: boolean;
|
|
14
16
|
remotePublication?: "off" | "turn-end" | "transition";
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -21,6 +23,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir()): StateFlowConfig {
|
|
|
21
23
|
directory: getDurableRepositoryRoot(agentDir),
|
|
22
24
|
autoStart: false,
|
|
23
25
|
logging: false,
|
|
26
|
+
showSuccessfulPatches: true,
|
|
24
27
|
};
|
|
25
28
|
let value: unknown;
|
|
26
29
|
try {
|
|
@@ -29,12 +32,13 @@ export function loadStateFlowConfig(agentDir = getAgentDir()): StateFlowConfig {
|
|
|
29
32
|
} catch (error) {
|
|
30
33
|
throw new Error(`Cannot read State Flow configuration: ${path}`, { cause: error });
|
|
31
34
|
}
|
|
32
|
-
const allowed = new Set(["directory", "autoStart", "logging", "remotePublication"]);
|
|
35
|
+
const allowed = new Set(["directory", "autoStart", "logging", "showSuccessfulPatches", "remotePublication"]);
|
|
33
36
|
if (!isObject(value) || Object.keys(value).some((key) => !allowed.has(key))) {
|
|
34
37
|
throw new Error(`State Flow configuration contains unknown settings: ${path}`);
|
|
35
38
|
}
|
|
36
39
|
if (Object.hasOwn(value, "autoStart") && typeof value.autoStart !== "boolean") throw new Error(`State Flow autoStart must be a boolean: ${path}`);
|
|
37
40
|
if (Object.hasOwn(value, "logging") && typeof value.logging !== "boolean") throw new Error(`State Flow logging must be a boolean: ${path}`);
|
|
41
|
+
if (Object.hasOwn(value, "showSuccessfulPatches") && typeof value.showSuccessfulPatches !== "boolean") throw new Error(`State Flow showSuccessfulPatches must be a boolean: ${path}`);
|
|
38
42
|
if (Object.hasOwn(value, "remotePublication") && value.remotePublication !== "off" && value.remotePublication !== "turn-end" && value.remotePublication !== "transition") {
|
|
39
43
|
throw new Error(`State Flow remotePublication must be off, turn-end, or transition: ${path}`);
|
|
40
44
|
}
|
|
@@ -48,6 +52,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir()): StateFlowConfig {
|
|
|
48
52
|
directory: expanded === undefined ? defaults.directory : resolve(dirname(path), expanded),
|
|
49
53
|
autoStart: value.autoStart === true,
|
|
50
54
|
logging: value.logging === true,
|
|
55
|
+
showSuccessfulPatches: value.showSuccessfulPatches !== false,
|
|
51
56
|
...(value.remotePublication === undefined ? {} : { remotePublication: value.remotePublication as "off" | "turn-end" | "transition" }),
|
|
52
57
|
};
|
|
53
58
|
}
|
|
@@ -18,22 +18,26 @@ import {
|
|
|
18
18
|
serializeArtifactProvenanceRegistry,
|
|
19
19
|
type ArtifactProvenanceRegistry,
|
|
20
20
|
} from "./artifact.ts";
|
|
21
|
-
import { canonicalJson,
|
|
21
|
+
import { canonicalJson, isJsonValue, isObject } from "./json.ts";
|
|
22
22
|
import { validateScopeStream, validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
23
|
-
import
|
|
23
|
+
import type { StateScope } from "./state.ts";
|
|
24
24
|
|
|
25
|
-
const LEGACY_MAX_SCOPE_SLUG_LENGTH = 80;
|
|
26
|
-
const LEGACY_SCOPE_KEY_PATTERN = /^[A-Za-z0-9._-]{1,80}-[a-f0-9]{64}$/;
|
|
27
25
|
const SESSION_KEY_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
28
26
|
const STATE_FILE = "state.json";
|
|
29
27
|
const CHECKPOINT_FILE = "checkpoint.json";
|
|
30
28
|
const PATCHES_FILE = "patches.jsonl";
|
|
31
29
|
const META_FILE = "meta.json";
|
|
32
30
|
|
|
33
|
-
/** Canonical
|
|
31
|
+
/** Canonical semantic sources plus runtime-owned temporal metadata. */
|
|
34
32
|
export interface ScopeStreamSources {
|
|
35
33
|
checkpoint: string;
|
|
36
34
|
patches: string;
|
|
35
|
+
temporal: ScopeTemporalMetadata;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScopeTemporalMetadata {
|
|
39
|
+
checkpoint: ScopeStream["checkpoint"]["through"];
|
|
40
|
+
patches: ScopeStream["patches"][number]["transition"][];
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface SessionAddress {
|
|
@@ -41,17 +45,18 @@ export interface SessionAddress {
|
|
|
41
45
|
readonly key: string;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
/**
|
|
48
|
+
/** Semantic files contain no runtime envelope; temporal boundaries and CWD ownership live in meta.json. */
|
|
45
49
|
export function serializeScopeStream(stream: ScopeStream, scope: StateScope, cwdIdentity?: string): ScopeStreamSources {
|
|
46
50
|
validateScopeStream(stream, scope);
|
|
47
51
|
if (scope === "cwd" && cwdIdentity === undefined) throw new Error("State Flow CWD scope serialization requires its canonical identity");
|
|
48
52
|
if (scope !== "cwd" && cwdIdentity !== undefined) throw new Error("Only State Flow CWD scope serialization accepts a CWD identity");
|
|
49
|
-
const checkpoint = scope === "cwd"
|
|
50
|
-
? { ...stream.checkpoint, owner: { cwd: resolve(cwdIdentity!) } }
|
|
51
|
-
: stream.checkpoint;
|
|
52
53
|
return {
|
|
53
|
-
checkpoint: `${canonicalJson(checkpoint)}\n`,
|
|
54
|
-
patches: stream.patches.map((record) => `${canonicalJson(record)}\n`).join(""),
|
|
54
|
+
checkpoint: `${canonicalJson(stream.checkpoint.state)}\n`,
|
|
55
|
+
patches: stream.patches.map((record) => `${canonicalJson(record.patch)}\n`).join(""),
|
|
56
|
+
temporal: {
|
|
57
|
+
checkpoint: structuredClone(stream.checkpoint.through),
|
|
58
|
+
patches: stream.patches.map((record) => structuredClone(record.transition)),
|
|
59
|
+
},
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -65,6 +70,7 @@ export function classifyScopeStream(
|
|
|
65
70
|
patchesSource: string | undefined,
|
|
66
71
|
scope: StateScope,
|
|
67
72
|
expectedCwd?: string,
|
|
73
|
+
metaSource?: string,
|
|
68
74
|
): ScopeStreamPresence {
|
|
69
75
|
if (checkpointSource === undefined && patchesSource === undefined) return { kind: "absent" };
|
|
70
76
|
if (checkpointSource === undefined || patchesSource === undefined) {
|
|
@@ -76,16 +82,11 @@ export function classifyScopeStream(
|
|
|
76
82
|
} catch {
|
|
77
83
|
throw new Error(`State Flow ${scope} checkpoint contains invalid JSON`);
|
|
78
84
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (expectedCwd !== undefined && (owner as { cwd: string }).cwd !== resolve(expectedCwd)) throw new Error("State Flow CWD scope identity mismatch");
|
|
86
|
-
checkpoint = semantic;
|
|
87
|
-
} else if (scope === "cwd" && expectedCwd !== undefined) {
|
|
88
|
-
throw new Error("State Flow CWD scope identity is missing");
|
|
85
|
+
let meta: Record<string, unknown> | undefined;
|
|
86
|
+
if (metaSource !== undefined) {
|
|
87
|
+
try { meta = JSON.parse(metaSource) as Record<string, unknown>; }
|
|
88
|
+
catch { throw new Error(`State Flow ${scope} metadata contains invalid JSON`); }
|
|
89
|
+
if (!isObject(meta)) throw new Error(`Invalid State Flow ${scope} metadata`);
|
|
89
90
|
}
|
|
90
91
|
const patches: unknown[] = [];
|
|
91
92
|
for (const [index, line] of patchesSource.split(/\r?\n/).entries()) {
|
|
@@ -96,7 +97,35 @@ export function classifyScopeStream(
|
|
|
96
97
|
throw new Error(`State Flow ${scope} tail contains invalid JSON at line ${index + 1}`);
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
const
|
|
100
|
+
const temporal = meta?.temporal;
|
|
101
|
+
if (isObject(temporal) && Object.hasOwn(temporal, "checkpoint") && Array.isArray(temporal.patches)) {
|
|
102
|
+
const owner = meta?.owner;
|
|
103
|
+
if (scope === "cwd" && expectedCwd !== undefined
|
|
104
|
+
&& (!isObject(owner) || Object.keys(owner).join(",") !== "cwd" || owner.cwd !== resolve(expectedCwd))) {
|
|
105
|
+
throw new Error(owner === undefined ? "State Flow CWD scope identity is missing" : "State Flow CWD scope identity mismatch");
|
|
106
|
+
}
|
|
107
|
+
const boundaries = temporal.patches;
|
|
108
|
+
if (boundaries.length !== patches.length) throw new Error(`State Flow ${scope} temporal metadata does not match its semantic tail`);
|
|
109
|
+
const stream = {
|
|
110
|
+
checkpoint: { through: temporal.checkpoint, state: checkpoint },
|
|
111
|
+
patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
|
|
112
|
+
};
|
|
113
|
+
validateScopeStream(stream, scope);
|
|
114
|
+
return { kind: "present", stream };
|
|
115
|
+
}
|
|
116
|
+
// Complete predecessor envelopes are the only no-meta form that may be unwrapped.
|
|
117
|
+
let legacyCheckpoint = checkpoint;
|
|
118
|
+
if (isObject(checkpoint) && Object.hasOwn(checkpoint, "owner")) {
|
|
119
|
+
const { owner, ...semantic } = checkpoint;
|
|
120
|
+
if (scope !== "cwd" || !isObject(owner) || Object.keys(owner).join(",") !== "cwd" || typeof owner.cwd !== "string") {
|
|
121
|
+
throw new Error("Invalid State Flow CWD scope identity");
|
|
122
|
+
}
|
|
123
|
+
if (expectedCwd !== undefined && owner.cwd !== resolve(expectedCwd)) throw new Error("State Flow CWD scope identity mismatch");
|
|
124
|
+
legacyCheckpoint = semantic;
|
|
125
|
+
} else if (scope === "cwd" && expectedCwd !== undefined) {
|
|
126
|
+
throw new Error("State Flow CWD scope identity is missing");
|
|
127
|
+
}
|
|
128
|
+
const stream = { checkpoint: legacyCheckpoint, patches };
|
|
100
129
|
validateScopeStream(stream, scope);
|
|
101
130
|
return { kind: "present", stream };
|
|
102
131
|
}
|
|
@@ -107,8 +136,9 @@ export function parseScopeStream(
|
|
|
107
136
|
patchesSource: string | undefined,
|
|
108
137
|
scope: StateScope,
|
|
109
138
|
expectedCwd?: string,
|
|
139
|
+
metaSource?: string,
|
|
110
140
|
): ScopeStream | undefined {
|
|
111
|
-
const presence = classifyScopeStream(checkpointSource, patchesSource, scope, expectedCwd);
|
|
141
|
+
const presence = classifyScopeStream(checkpointSource, patchesSource, scope, expectedCwd, metaSource);
|
|
112
142
|
return presence.kind === "present" ? presence.stream : undefined;
|
|
113
143
|
}
|
|
114
144
|
|
|
@@ -134,13 +164,13 @@ export function sessionRuntimePaths(cwd: string, sessionId: string, repositoryRo
|
|
|
134
164
|
return { config: join(directory, "config.json"), meta: join(directory, META_FILE) };
|
|
135
165
|
}
|
|
136
166
|
|
|
137
|
-
/**
|
|
167
|
+
/** Unsupported state.json presence never becomes an anchored checkpoint. */
|
|
138
168
|
export function loadScopeStream(cwd: string, sessionId: string, scope: StateScope, repositoryRoot: string, sessionKey = sessionId): ScopeStream | undefined {
|
|
139
169
|
const paths = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
|
|
140
170
|
if (readRegularBytes(join(paths.directory, STATE_FILE), repositoryRoot) !== undefined) {
|
|
141
171
|
throw new Error(`Legacy State Flow storage requires explicit migration: ${paths.directory}`);
|
|
142
172
|
}
|
|
143
|
-
return parseScopeStream(readRegularFile(paths.checkpoint, repositoryRoot), readRegularFile(paths.patches, repositoryRoot), scope, scope === "cwd" ? cwd : undefined);
|
|
173
|
+
return parseScopeStream(readRegularFile(paths.checkpoint, repositoryRoot), readRegularFile(paths.patches, repositoryRoot), scope, scope === "cwd" ? cwd : undefined, readRegularFile(paths.meta, repositoryRoot));
|
|
144
174
|
}
|
|
145
175
|
|
|
146
176
|
/** Include legacy names in the CAS basis solely to prevent format races during cutover. */
|
|
@@ -182,24 +212,43 @@ export interface DurablePaths {
|
|
|
182
212
|
globalPatches: string;
|
|
183
213
|
}
|
|
184
214
|
|
|
185
|
-
/**
|
|
215
|
+
/** Merge authoritative owned leaves while preserving forward-compatible metadata siblings. */
|
|
216
|
+
export function serializeScopeMetadata(
|
|
217
|
+
registry: Readonly<ArtifactProvenanceRegistry> | undefined, stream: ScopeStream, scope: StateScope,
|
|
218
|
+
cwdIdentity?: string, existingSource?: string,
|
|
219
|
+
): string {
|
|
220
|
+
const existing = parseMetadataDocument(existingSource, `State Flow metadata`);
|
|
221
|
+
const sources = serializeScopeStream(stream, scope, cwdIdentity);
|
|
222
|
+
const value = {
|
|
223
|
+
...existing,
|
|
224
|
+
version: 1,
|
|
225
|
+
...(registry === undefined ? {} : { artifacts: serializeArtifactProvenanceRegistry(registry) }),
|
|
226
|
+
temporal: sources.temporal,
|
|
227
|
+
...(scope === "cwd" ? { owner: { cwd: resolve(cwdIdentity!) } } : {}),
|
|
228
|
+
};
|
|
229
|
+
return `${canonicalJson(value)}\n`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Compatibility serializer retained for metadata-only callers. */
|
|
186
233
|
export function serializeScopeProvenance(registry: Readonly<ArtifactProvenanceRegistry>): string {
|
|
187
234
|
return `${canonicalJson({ version: 1, artifacts: serializeArtifactProvenanceRegistry(registry) })}\n`;
|
|
188
235
|
}
|
|
189
236
|
|
|
190
|
-
|
|
191
|
-
export function parseScopeProvenance(source: string | undefined, path: string): ArtifactProvenanceRegistry {
|
|
237
|
+
function parseMetadataDocument(source: string | undefined, label: string): Record<string, unknown> {
|
|
192
238
|
if (source === undefined) return {};
|
|
193
239
|
let value: unknown;
|
|
194
|
-
try {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
240
|
+
try { value = JSON.parse(source); }
|
|
241
|
+
catch { throw new Error(`${label} contains invalid JSON`); }
|
|
242
|
+
if (!isObject(value) || !isJsonValue(value)) throw new Error(`Invalid ${label}`);
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Missing provenance is unavailable evidence, never corrupt state. Unknown metadata is preserved by writers. */
|
|
247
|
+
export function parseScopeProvenance(source: string | undefined, path: string): ArtifactProvenanceRegistry {
|
|
248
|
+
const value = parseMetadataDocument(source, `State Flow provenance file: ${path}`);
|
|
249
|
+
if (Object.keys(value).length === 0) return {};
|
|
250
|
+
if (value.version !== 1) throw new Error(`Invalid State Flow provenance document: ${path}`);
|
|
251
|
+
if (!Object.hasOwn(value, "artifacts")) return {};
|
|
203
252
|
return parseArtifactProvenanceRegistry(value.artifacts, `State Flow provenance at ${path}`);
|
|
204
253
|
}
|
|
205
254
|
|
|
@@ -232,12 +281,6 @@ export function durablePaths(repositoryRoot = getDurableRepositoryRoot()): Durab
|
|
|
232
281
|
};
|
|
233
282
|
}
|
|
234
283
|
|
|
235
|
-
function legacyReadableScopeKey(identity: string, readable: string): string {
|
|
236
|
-
if (identity.length === 0) throw new Error("State Flow scope identity must be non-empty");
|
|
237
|
-
const slug = readable.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, LEGACY_MAX_SCOPE_SLUG_LENGTH) || "scope";
|
|
238
|
-
return `${slug}-${createHash("sha256").update(identity).digest("hex")}`;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
284
|
/** Match Pi's native project-session directory convention exactly. */
|
|
242
285
|
export function cwdScopeKey(cwd: string): string {
|
|
243
286
|
const canonical = resolve(cwd);
|
|
@@ -266,16 +309,6 @@ export function resolveSessionAddress(sessionFile: string | undefined, sessionId
|
|
|
266
309
|
return Object.freeze({ id: sessionId, key: sessionStorageKey(sessionFile, sessionId, timestamp) });
|
|
267
310
|
}
|
|
268
311
|
|
|
269
|
-
/** Read-only migration input for the untagged hashed-layout draft. */
|
|
270
|
-
export function legacyCwdScopeKey(cwd: string): string {
|
|
271
|
-
const canonical = resolve(cwd);
|
|
272
|
-
return legacyReadableScopeKey(canonical, `--${canonical.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function legacySessionScopeKey(sessionId: string): string {
|
|
276
|
-
return legacyReadableScopeKey(sessionScopeKey(sessionId), sessionId);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
312
|
export function cwdScopePaths(cwd: string, repositoryRoot = getDurableRepositoryRoot()): ScopePaths {
|
|
280
313
|
const directory = join(resolve(repositoryRoot), cwdScopeKey(cwd));
|
|
281
314
|
return { directory, state: join(directory, STATE_FILE), patches: join(directory, PATCHES_FILE) };
|
|
@@ -299,28 +332,6 @@ export function cwdPatchesPath(cwd: string, repositoryRoot = getDurableRepositor
|
|
|
299
332
|
return cwdScopePaths(cwd, repositoryRoot).patches;
|
|
300
333
|
}
|
|
301
334
|
|
|
302
|
-
/** Historical paths from the pre-0.4 hashed-layout draft; never selected for new writes. */
|
|
303
|
-
export function legacyTemporalScopePaths(cwd: string, sessionId: string, scope: StateScope, repositoryRoot: string): TemporalScopePaths {
|
|
304
|
-
const root = resolve(repositoryRoot);
|
|
305
|
-
const cwdDirectory = join(root, legacyCwdScopeKey(cwd));
|
|
306
|
-
const directory = scope === "global" ? root : scope === "cwd" ? cwdDirectory : join(cwdDirectory, legacySessionScopeKey(sessionId));
|
|
307
|
-
return { directory, checkpoint: join(directory, CHECKPOINT_FILE), patches: join(directory, PATCHES_FILE), meta: join(directory, META_FILE) };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function legacySessionRuntimePaths(cwd: string, sessionId: string, repositoryRoot: string): { config: string; meta: string } {
|
|
311
|
-
const directory = legacyTemporalScopePaths(cwd, sessionId, "session", repositoryRoot).directory;
|
|
312
|
-
return { config: join(directory, "config.json"), meta: join(directory, "meta.json") };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export function captureLegacyTemporalFileBases(cwd: string, sessionId: string, repositoryRoot: string): DurableFileBase[] {
|
|
316
|
-
const paths = (["global", "cwd", "session"] as const).flatMap((scope) => {
|
|
317
|
-
const pair = legacyTemporalScopePaths(cwd, sessionId, scope, repositoryRoot);
|
|
318
|
-
const runtime = scope === "session" ? legacySessionRuntimePaths(cwd, sessionId, repositoryRoot) : undefined;
|
|
319
|
-
return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [] : [runtime.config, runtime.meta])];
|
|
320
|
-
});
|
|
321
|
-
return captureOwnedFileBases(paths, repositoryRoot);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
335
|
export function sessionStatePath(cwd: string, sessionId: string, repositoryRoot = getDurableRepositoryRoot(), sessionKey = sessionId): string {
|
|
325
336
|
return sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).state;
|
|
326
337
|
}
|
|
@@ -337,7 +348,7 @@ export function isStateFlowOwnedPath(candidate: string, repositoryRoot = getDura
|
|
|
337
348
|
if (absolute === global.globalState || absolute === global.globalPatches
|
|
338
349
|
|| absolute === join(root, CHECKPOINT_FILE) || absolute === join(root, META_FILE)) return true;
|
|
339
350
|
const segments = relative(root, absolute).split(sep);
|
|
340
|
-
const cwdKey = (value: string) =>
|
|
351
|
+
const cwdKey = (value: string) => value.startsWith("--") && value.endsWith("--");
|
|
341
352
|
const sessionKey = (value: string) => {
|
|
342
353
|
try { return sessionScopeKey(value) === value; } catch { return false; }
|
|
343
354
|
};
|
|
@@ -346,7 +357,7 @@ export function isStateFlowOwnedPath(candidate: string, repositoryRoot = getDura
|
|
|
346
357
|
}
|
|
347
358
|
if (segments.length === 3) {
|
|
348
359
|
return cwdKey(segments[0]!)
|
|
349
|
-
&&
|
|
360
|
+
&& sessionKey(segments[1]!)
|
|
350
361
|
&& (segments[2] === STATE_FILE || segments[2] === CHECKPOINT_FILE || segments[2] === PATCHES_FILE
|
|
351
362
|
|| segments[2] === "config.json" || segments[2] === META_FILE);
|
|
352
363
|
}
|
|
@@ -446,25 +457,6 @@ export function captureOwnedFileBases(paths: readonly string[], repositoryRoot:
|
|
|
446
457
|
});
|
|
447
458
|
}
|
|
448
459
|
|
|
449
|
-
function validateState(value: unknown, path: string): asserts value is MaterializedState {
|
|
450
|
-
if (!isMaterializedState(value) || !isJsonValue(value) || containsNull(value)) {
|
|
451
|
-
throw new Error(`Durable State Flow file has invalid materialized state: ${path}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/** One-way legacy current-state interpretation; explanatory journals are not recovery input. */
|
|
456
|
-
export function parseStateSource(source: string | undefined, path: string): MaterializedState | undefined {
|
|
457
|
-
if (source === undefined) return undefined;
|
|
458
|
-
let value: unknown;
|
|
459
|
-
try {
|
|
460
|
-
value = JSON.parse(source);
|
|
461
|
-
} catch {
|
|
462
|
-
throw new Error(`Durable State Flow file contains invalid JSON: ${path}`);
|
|
463
|
-
}
|
|
464
|
-
validateState(value, path);
|
|
465
|
-
return structuredClone(value);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
460
|
interface PreparedFile {
|
|
469
461
|
path: string;
|
|
470
462
|
repositoryRoot: string;
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
6
7
|
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.ts";
|
|
7
8
|
import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation, type PassiveContinuation } from "./context.ts";
|
|
8
9
|
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
@@ -26,6 +27,7 @@ import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePa
|
|
|
26
27
|
import { runPublicationWorker } from "./publication.ts";
|
|
27
28
|
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.ts";
|
|
28
29
|
import { isObject, sameJson } from "./json.ts";
|
|
30
|
+
import { readStatePath } from "./query.ts";
|
|
29
31
|
import {
|
|
30
32
|
cwdScopeKey,
|
|
31
33
|
resolveSessionAddress,
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
planArtifactInvalidation,
|
|
41
43
|
type ArtifactInvalidationRequest,
|
|
42
44
|
} from "./artifact.ts";
|
|
43
|
-
import { planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
|
|
45
|
+
import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
|
|
44
46
|
|
|
45
47
|
export interface StateFlowExtensionOptions {
|
|
46
48
|
agentDir?: string;
|
|
@@ -55,6 +57,34 @@ export const READ_STATE_TOOL_NAME = "read_state";
|
|
|
55
57
|
export const MAX_FALLBACK_ATTEMPTS: number = 2;
|
|
56
58
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
57
59
|
const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
|
|
60
|
+
const PATCH_DISPLAY_SECTION_KEYS = new Set([
|
|
61
|
+
"global",
|
|
62
|
+
"cwd",
|
|
63
|
+
"session",
|
|
64
|
+
"artifacts",
|
|
65
|
+
"contract",
|
|
66
|
+
"working",
|
|
67
|
+
"response",
|
|
68
|
+
"final",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/** Keep successful patch JSON valid while separating adjacent scopes and memory sections visually. */
|
|
72
|
+
export function formatPatchStateArguments(args: unknown): string {
|
|
73
|
+
const seenAtIndent = new Set<number>();
|
|
74
|
+
return JSON.stringify(args, null, 2).split("\n").flatMap((line) => {
|
|
75
|
+
const indent = line.length - line.trimStart().length;
|
|
76
|
+
const match = /^(\s+)"([^"]+)":/.exec(line);
|
|
77
|
+
if (match === null || !PATCH_DISPLAY_SECTION_KEYS.has(match[2])) {
|
|
78
|
+
for (const seenIndent of seenAtIndent) {
|
|
79
|
+
if (seenIndent > indent) seenAtIndent.delete(seenIndent);
|
|
80
|
+
}
|
|
81
|
+
return [line];
|
|
82
|
+
}
|
|
83
|
+
const separator = seenAtIndent.has(indent) ? [""] : [];
|
|
84
|
+
seenAtIndent.add(indent);
|
|
85
|
+
return [...separator, line];
|
|
86
|
+
}).join("\n");
|
|
87
|
+
}
|
|
58
88
|
|
|
59
89
|
/** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
|
|
60
90
|
export function normalizePatchStateArguments(args: unknown): any {
|
|
@@ -70,10 +100,15 @@ export function normalizePatchStateArguments(args: unknown): any {
|
|
|
70
100
|
return { ...args, final };
|
|
71
101
|
}
|
|
72
102
|
|
|
103
|
+
/** Keep tool output visually separated from its heading with exactly one leading newline. */
|
|
104
|
+
function separatedOutput(text: string): string {
|
|
105
|
+
return `\n${text.replace(/^\n+/, "")}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
/** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
|
|
74
109
|
function separatedFailure(error: unknown): Error {
|
|
75
110
|
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
-
return new Error(
|
|
111
|
+
return new Error(separatedOutput(message), error instanceof Error ? { cause: error } : undefined);
|
|
77
112
|
}
|
|
78
113
|
|
|
79
114
|
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
@@ -560,9 +595,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
560
595
|
const branch = ctx.sessionManager.getBranch();
|
|
561
596
|
const discovery = discoverSnapshotData(branch);
|
|
562
597
|
let restoreSelected: (() => Snapshot) | undefined;
|
|
563
|
-
const recovery = recoverSnapshot(discovery.candidates, (revision
|
|
598
|
+
const recovery = recoverSnapshot(discovery.candidates, (revision) => {
|
|
564
599
|
try {
|
|
565
|
-
const prepared = prepareBranchRestore(ctx, revision
|
|
600
|
+
const prepared = prepareBranchRestore(ctx, revision);
|
|
566
601
|
restoreSelected = prepared.restore;
|
|
567
602
|
return prepared.snapshot;
|
|
568
603
|
} catch (error) {
|
|
@@ -586,7 +621,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
586
621
|
} else if (branchHasSnapshot && snapshot.config.enabled) {
|
|
587
622
|
const publication = runtime.initialize(snapshot, true);
|
|
588
623
|
recordPolicyPublication(publication, ctx);
|
|
589
|
-
delete snapshot.legacySession;
|
|
590
624
|
}
|
|
591
625
|
installScopeStates();
|
|
592
626
|
} else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
|
|
@@ -762,23 +796,33 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
762
796
|
pi.registerTool({
|
|
763
797
|
name: READ_STATE_TOOL_NAME,
|
|
764
798
|
label: "Read State",
|
|
765
|
-
description: "Read
|
|
766
|
-
promptSnippet: "Read
|
|
799
|
+
description: "Read State Flow through a unified path such as state, state[1], state.cwd[2], or state.global.patches[0]. The legacy offset/scope form remains accepted. Read-only and lazy; unavailable hot history is an error.",
|
|
800
|
+
promptSnippet: "Read cached state or accepted scope patches with a unified path",
|
|
767
801
|
parameters: Type.Object({
|
|
768
|
-
|
|
769
|
-
|
|
802
|
+
path: Type.Optional(Type.String({ description: "Unified query path; current aliases use index 0" })),
|
|
803
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: 7, description: "Legacy accepted-transition offset; defaults to 0" })),
|
|
804
|
+
scope: Type.Optional(StringEnum(["effective", "global", "cwd", "session"] as const, { description: "Legacy projection at the same boundary; defaults to effective" })),
|
|
770
805
|
}, { additionalProperties: false }),
|
|
771
806
|
async execute(_toolCallId, params, signal) {
|
|
772
807
|
try {
|
|
808
|
+
type ReadDetails = { path?: string; offset?: number; scope?: string; transitionId: string };
|
|
773
809
|
if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
|
|
774
810
|
if (signal?.aborted) throw new Error("State Flow read was aborted");
|
|
775
811
|
if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
812
|
+
if (params.path !== undefined) {
|
|
813
|
+
if (params.offset !== undefined || params.scope !== undefined) throw new Error("read_state path cannot be combined with legacy offset or scope");
|
|
814
|
+
const result = readStatePath(runtime.view, params.path);
|
|
815
|
+
return {
|
|
816
|
+
content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
|
|
817
|
+
details: { path: params.path, transitionId: result.boundary.id } as ReadDetails,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
776
820
|
const { offset = 0, scope = "effective" } = params;
|
|
777
821
|
const state = runtime.read(offset, scope === "effective" ? undefined : scope);
|
|
778
822
|
const boundary = runtime.view.lineage.at(-1 - offset)!;
|
|
779
823
|
return {
|
|
780
824
|
content: [{ type: "text", text: `\n${JSON.stringify({ offset, scope, boundary, state: projectStateForModel(state) })}` }],
|
|
781
|
-
details: { offset, scope, transitionId: boundary.id },
|
|
825
|
+
details: { offset, scope, transitionId: boundary.id } as ReadDetails,
|
|
782
826
|
};
|
|
783
827
|
} catch (error) {
|
|
784
828
|
throw separatedFailure(error);
|
|
@@ -804,6 +848,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
804
848
|
final: Type.Optional(Type.Boolean({ description: "Set exactly true to permit this iteration to finish at a later turn_end" })),
|
|
805
849
|
}, { additionalProperties: false }),
|
|
806
850
|
prepareArguments: normalizePatchStateArguments,
|
|
851
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
852
|
+
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
|
853
|
+
if (context.isError) return new Text(separatedOutput(text), 0, 0);
|
|
854
|
+
if (isPartial || !config.showSuccessfulPatches) return new Text(text, 0, 0);
|
|
855
|
+
return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
|
|
856
|
+
},
|
|
807
857
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
808
858
|
try {
|
|
809
859
|
if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
|
|
@@ -874,6 +924,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
874
924
|
activeContext = ctx;
|
|
875
925
|
runtime ??= createRuntime(ctx);
|
|
876
926
|
if (branchStartsWithoutRuntime) runtime.prepare();
|
|
927
|
+
runtime.migrateLegacyStorage();
|
|
877
928
|
const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
|
|
878
929
|
&& (hasPriorConversation(branch) || previousPassiveContinuation !== undefined);
|
|
879
930
|
if (!runtime.view && snapshot.meta.durableBase) {
|
|
@@ -896,7 +947,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
896
947
|
: runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
|
|
897
948
|
recordPolicyPublication(publication, ctx);
|
|
898
949
|
installScopeStates();
|
|
899
|
-
delete snapshot.legacySession;
|
|
900
950
|
clearRunTransient();
|
|
901
951
|
passiveContinuation = undefined;
|
|
902
952
|
bootstrapContinuation = snapshot.meta.bootstrap
|
|
@@ -1195,7 +1245,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1195
1245
|
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
|
|
1196
1246
|
|| !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
|
|
1197
1247
|
completedRunAccepted = false;
|
|
1198
|
-
const
|
|
1248
|
+
const entries = ctx.sessionManager.buildContextEntries();
|
|
1249
|
+
if (!hasCompactionSizedTranscript(entries)) return;
|
|
1250
|
+
const plan = planStateFlowCompaction(entries, snapshot.meta.durableBase, snapshot.meta.step);
|
|
1199
1251
|
if (!plan) return;
|
|
1200
1252
|
compactionPlan = plan;
|
|
1201
1253
|
compactionInFlight = true;
|