@llblab/pi-kit 0.10.8 → 0.11.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 +14 -10
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +9 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -0
- package/node_modules/@llblab/pi-state-flow/README.md +52 -261
- package/node_modules/@llblab/pi-state-flow/docs/README.md +6 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +47 -26
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +97 -0
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +35 -0
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +47 -0
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +459 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +35 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +142 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +19 -3
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +74 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +12 -12
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +21 -5
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -5
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +146 -37
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +142 -42
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +80 -27
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +157 -20
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +7 -12
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +2 -3
- package/node_modules/@llblab/pi-state-flow/package.json +5 -4
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { lstatSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths, type SessionAddress } from "./durable.ts";
|
|
4
|
+
import { classifyScopeStream, parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths, type SessionAddress } from "./durable.ts";
|
|
5
5
|
import { parseArtifactProvenanceRegistry, pruneArtifactProvenance, type ArtifactProvenance, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
6
6
|
import { adoptFileStateToGit, initializeGitRepository, isLocalGitRepository, captureTemporalGitBase, loadLegacyStatesAtRevision, loadTemporalRevision, migrateHashedCwdAtHead, migrateHashedLayoutAtHead, migrateLegacyStorageToGit, publishTemporalStateToGit, type TemporalGitBase } from "./git.ts";
|
|
7
7
|
import { captureTemporalFileBase, detectGitCapability, initializeFileStore, loadTemporalFileRevision, migrateLegacyStorageToFiles, publishTemporalStateToFiles, type TemporalFileBase } from "./storage.ts";
|
|
@@ -16,6 +16,12 @@ const SCOPES = ["global", "cwd", "session"] as const;
|
|
|
16
16
|
const SHARED_SCOPES = ["global", "cwd"] as const;
|
|
17
17
|
export type RuntimePublication = ReturnType<typeof publishTemporalStateToGit> & { revision?: string };
|
|
18
18
|
|
|
19
|
+
interface SessionCopy {
|
|
20
|
+
stream: ScopeStream;
|
|
21
|
+
provenance: ArtifactProvenanceRegistry;
|
|
22
|
+
backend: "git" | "files";
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
function emptyProvenance(): Record<StateScope, ArtifactProvenanceRegistry> {
|
|
20
26
|
return { global: {}, cwd: {}, session: {} };
|
|
21
27
|
}
|
|
@@ -33,10 +39,27 @@ function targetScopeConflict(scopes: readonly StateScope[]): Error {
|
|
|
33
39
|
return new Error(`State Flow cannot publish the ${labels.join(" and ")} patches because the live ${labels.join(" and ")} states advanced after this transition's selected basis. Refresh or reconcile the target scopes before retrying.`);
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
/** Disappearance invalidates a selected write target even though untouched scopes can adopt empty reality. */
|
|
43
|
+
function removedTargetScopeConflict(scopes: readonly StateScope[]): Error {
|
|
44
|
+
const labels = scopes.map(scopeLabel);
|
|
45
|
+
if (labels.length === 1) {
|
|
46
|
+
return new Error(`State Flow cannot publish the ${labels[0]} patch because the live ${labels[0]} scope was removed after this transition's selected basis. Refresh or reconcile the target scope before retrying.`);
|
|
47
|
+
}
|
|
48
|
+
return new Error(`State Flow cannot publish the ${labels.join(" and ")} patches because the live ${labels.join(" and ")} scopes were removed after this transition's selected basis. Refresh or reconcile the target scopes before retrying.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function freshEmptyScopeStream(scope: StateScope, origin: string): ScopeStream {
|
|
52
|
+
return createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, origin).scopes[scope];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class MissingSessionRuntimeError extends Error {
|
|
56
|
+
constructor() { super("Linked State Flow revision has no session runtime"); }
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
/** Immutable target validation is independent of acquiring the live publication basis. */
|
|
37
60
|
export function inspectRuntimeRevision(cwd: string, sessionId: string, root: string, revision: string, sessionKey = sessionId) {
|
|
38
61
|
const loaded = loadTemporalRevision(cwd, sessionId, root, revision, sessionKey);
|
|
39
|
-
if (!loaded.runtime) throw new
|
|
62
|
+
if (!loaded.runtime) throw new MissingSessionRuntimeError();
|
|
40
63
|
const resolved = resolveSessionRuntime(loaded.runtime.document, loaded.runtime.revision);
|
|
41
64
|
if (!loaded.scopes.global || !loaded.scopes.cwd || !loaded.scopes.session) throw new Error("Incomplete temporal scope cohort");
|
|
42
65
|
const view = { lineage: resolved.lineage, scopes: { global: loaded.scopes.global, cwd: loaded.scopes.cwd, session: loaded.scopes.session } };
|
|
@@ -71,6 +94,8 @@ export class TemporalRuntime {
|
|
|
71
94
|
private savedRuntime: string | undefined;
|
|
72
95
|
private backend: "git" | "files" | undefined;
|
|
73
96
|
private provenanceByScope: Record<StateScope, ArtifactProvenanceRegistry> = emptyProvenance();
|
|
97
|
+
/** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
|
|
98
|
+
private readonly absentSharedScopes = new Set<StateScope>();
|
|
74
99
|
readonly cwd: string;
|
|
75
100
|
private readonly session: SessionAddress;
|
|
76
101
|
readonly root: string;
|
|
@@ -109,6 +134,7 @@ export class TemporalRuntime {
|
|
|
109
134
|
this.backend = "git";
|
|
110
135
|
this.semanticRevision = result.revision;
|
|
111
136
|
this.savedRuntime = savedRuntime;
|
|
137
|
+
this.absentSharedScopes.clear();
|
|
112
138
|
return result;
|
|
113
139
|
}
|
|
114
140
|
|
|
@@ -138,8 +164,66 @@ export class TemporalRuntime {
|
|
|
138
164
|
return result;
|
|
139
165
|
}
|
|
140
166
|
|
|
141
|
-
|
|
167
|
+
/** Validate selection without installing state; only a matching immutable Git inspection is reusable. */
|
|
168
|
+
prepareRestore(revision: string, legacySnapshot?: Snapshot): { snapshot: Snapshot; restore: () => Snapshot } {
|
|
142
169
|
const inspected = inspectSnapshotRevision(this.cwd, this.sessionId, this.root, revision, legacySnapshot, this.sessionKey);
|
|
170
|
+
const selected = inspected.snapshot.meta.durableBase ?? revision;
|
|
171
|
+
let consumed = false;
|
|
172
|
+
return {
|
|
173
|
+
snapshot: structuredClone(inspected.snapshot),
|
|
174
|
+
restore: () => {
|
|
175
|
+
if (consumed) throw new Error("Prepared State Flow restore was already consumed");
|
|
176
|
+
consumed = true;
|
|
177
|
+
// File cohorts can expire; legacy migration and owner redirection keep their fresh-read path.
|
|
178
|
+
return inspected.temporal && selected === revision
|
|
179
|
+
? this.restoreInspected(revision, inspected)
|
|
180
|
+
: this.restore(selected, inspected.snapshot);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Copy only a proven source session stream; shared streams come from the child's fresh live basis. */
|
|
186
|
+
prepareFork(source: SessionAddress, revision: string): { snapshot: Snapshot; fork: () => { snapshot: Snapshot; publication: RuntimePublication } } {
|
|
187
|
+
const parent = Object.freeze({ ...source });
|
|
188
|
+
if (parent.id === this.sessionId || parent.key === this.sessionKey) throw new Error("State Flow fork requires a distinct session identity and key");
|
|
189
|
+
const inspect = () => inspectSnapshotRevision(this.cwd, parent.id, this.root, revision, undefined, parent.key);
|
|
190
|
+
const inspected = inspect();
|
|
191
|
+
const snapshot: Snapshot = {
|
|
192
|
+
config: structuredClone(inspected.snapshot.config),
|
|
193
|
+
meta: {
|
|
194
|
+
step: 0,
|
|
195
|
+
...(inspected.snapshot.meta.bootstrap === undefined ? {} : { bootstrap: inspected.snapshot.meta.bootstrap }),
|
|
196
|
+
...(inspected.snapshot.meta.remotePublication === undefined ? {} : { remotePublication: structuredClone(inspected.snapshot.meta.remotePublication) }),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
let consumed = false;
|
|
200
|
+
return {
|
|
201
|
+
snapshot: { ...structuredClone(snapshot), meta: { ...structuredClone(snapshot.meta), durableBase: revision } },
|
|
202
|
+
fork: () => {
|
|
203
|
+
if (consumed) throw new Error("Prepared State Flow fork was already consumed");
|
|
204
|
+
consumed = true;
|
|
205
|
+
if (this.view) throw new Error("State Flow fork target already has session storage");
|
|
206
|
+
// Unlike immutable Git input, a file-only source must still match its complete cohort.
|
|
207
|
+
const current = inspected.file ? inspect() : inspected;
|
|
208
|
+
const selected = current.temporal ?? current.file;
|
|
209
|
+
if (!selected) throw new Error("State Flow fork requires a temporal session stream");
|
|
210
|
+
const publication = this.initializeOrigin(snapshot, { allowCreateCwd: false, copy: {
|
|
211
|
+
stream: selected.view.scopes.session,
|
|
212
|
+
provenance: selected.provenance.session,
|
|
213
|
+
backend: current.file ? "files" : "git",
|
|
214
|
+
} });
|
|
215
|
+
const ownedRevision = publication?.revision ?? publication?.commit;
|
|
216
|
+
if (!publication || !ownedRevision) throw new Error("State Flow fork requires existing shared scope storage");
|
|
217
|
+
return { snapshot: { ...structuredClone(snapshot), meta: { ...structuredClone(snapshot.meta), durableBase: ownedRevision } }, publication };
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
restore(revision: string, legacySnapshot?: Snapshot): Snapshot {
|
|
223
|
+
return this.restoreInspected(revision, inspectSnapshotRevision(this.cwd, this.sessionId, this.root, revision, legacySnapshot, this.sessionKey));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private restoreInspected(revision: string, inspected: ReturnType<typeof inspectSnapshotRevision>): Snapshot {
|
|
143
227
|
if (inspected.file) {
|
|
144
228
|
const savedRuntime = hashJson(createSessionRuntime(inspected.snapshot, this.cwd, this.sessionId, inspected.file.view.lineage, "files", inspected.file.provenance.session));
|
|
145
229
|
this.view = inspected.file.view;
|
|
@@ -148,6 +232,7 @@ export class TemporalRuntime {
|
|
|
148
232
|
this.provenanceByScope = structuredClone(inspected.file.provenance);
|
|
149
233
|
this.semanticRevision = revision;
|
|
150
234
|
this.savedRuntime = savedRuntime;
|
|
235
|
+
this.absentSharedScopes.clear();
|
|
151
236
|
return inspected.snapshot;
|
|
152
237
|
}
|
|
153
238
|
if (!inspected.temporal) {
|
|
@@ -201,20 +286,31 @@ export class TemporalRuntime {
|
|
|
201
286
|
this.provenanceByScope = structuredClone(loaded.provenance);
|
|
202
287
|
this.semanticRevision = semanticRevision;
|
|
203
288
|
this.savedRuntime = savedRuntime;
|
|
289
|
+
this.absentSharedScopes.clear();
|
|
204
290
|
return resolved.snapshot;
|
|
205
291
|
}
|
|
206
292
|
|
|
207
293
|
initialize(snapshot: Snapshot, allowCreateCwd: boolean, expectedShared?: Pick<ScopedStates, "global" | "cwd">, newSessionOrigin = false): RuntimePublication | undefined {
|
|
294
|
+
return this.initializeOrigin(snapshot, { allowCreateCwd, expectedShared, newSessionOrigin });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private initializeOrigin(snapshot: Snapshot, options: {
|
|
298
|
+
allowCreateCwd: boolean;
|
|
299
|
+
expectedShared?: Pick<ScopedStates, "global" | "cwd">;
|
|
300
|
+
newSessionOrigin?: boolean;
|
|
301
|
+
copy?: SessionCopy;
|
|
302
|
+
}): RuntimePublication | undefined {
|
|
303
|
+
const { allowCreateCwd, expectedShared, newSessionOrigin = false, copy } = options;
|
|
208
304
|
const hasCwd = hasCwdMaterialization(this.cwd, this.root);
|
|
209
305
|
if (!allowCreateCwd && !hasCwd) return undefined;
|
|
210
|
-
const backend = this.backend ?? (detectGitCapability() === "git" && lstatSync(join(this.root, ".git"), { throwIfNoEntry: false }) ? "git" : "files");
|
|
211
|
-
if (backend === "git") {
|
|
306
|
+
const backend = copy?.backend ?? this.backend ?? (detectGitCapability() === "git" && lstatSync(join(this.root, ".git"), { throwIfNoEntry: false }) ? "git" : "files");
|
|
307
|
+
if (!copy && backend === "git") {
|
|
212
308
|
if (!hasCwd) migrateHashedCwdAtHead(this.cwd, this.root);
|
|
213
309
|
if (hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey)) {
|
|
214
310
|
migrateLegacyStorageToGit(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
215
311
|
}
|
|
216
312
|
}
|
|
217
|
-
else {
|
|
313
|
+
else if (!copy) {
|
|
218
314
|
if (allowCreateCwd) initializeFileStore(this.root);
|
|
219
315
|
if (hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey)) {
|
|
220
316
|
migrateLegacyStorageToFiles(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
@@ -222,6 +318,15 @@ export class TemporalRuntime {
|
|
|
222
318
|
}
|
|
223
319
|
const base: TemporalGitBase = backend === "git" ? captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey) : captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
224
320
|
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
321
|
+
if (copy) {
|
|
322
|
+
const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
|
|
323
|
+
const owned = [session.checkpoint, session.patches, session.meta, join(session.directory, "config.json"), join(session.directory, "state.json")];
|
|
324
|
+
const occupied = owned.some((path) => files.get(path) !== undefined);
|
|
325
|
+
const historical = !occupied && backend === "git" && base.head ? loadTemporalRevision(this.cwd, this.sessionId, this.root, base.head, this.sessionKey) : undefined;
|
|
326
|
+
if (occupied || historical?.runtime || historical?.scopes.session) {
|
|
327
|
+
throw new Error("State Flow fork target already has session storage");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
225
330
|
if (SCOPES.some((scope) => {
|
|
226
331
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
227
332
|
return files.get(join(paths.directory, "state.json")) !== undefined;
|
|
@@ -231,11 +336,13 @@ export class TemporalRuntime {
|
|
|
231
336
|
return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined)];
|
|
232
337
|
})) as Record<StateScope, TemporalState["scopes"][StateScope] | undefined>;
|
|
233
338
|
if (!streams.cwd && !allowCreateCwd) return undefined;
|
|
339
|
+
if (copy && !streams.global) throw new Error("State Flow fork requires existing shared scope storage");
|
|
234
340
|
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
235
341
|
const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.meta), this.cwd, this.sessionId);
|
|
236
342
|
if (existingRuntime && !snapshot.legacySession && !newSessionOrigin) throw new Error("Existing session runtime requires a branch revision pointer");
|
|
237
343
|
// Explicit start before any branch runtime is a new origin, never inheritance of a later session layer.
|
|
238
344
|
if (snapshot.legacySession || newSessionOrigin) streams.session = undefined;
|
|
345
|
+
if (copy) streams.session = copy.stream;
|
|
239
346
|
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: snapshot.legacySession?.state ?? emptyState() }, randomUUID());
|
|
240
347
|
const candidate = new TemporalRuntime(this.cwd, this.session, this.root);
|
|
241
348
|
candidate.backend = backend;
|
|
@@ -246,18 +353,32 @@ export class TemporalRuntime {
|
|
|
246
353
|
candidate.provenanceByScope = {
|
|
247
354
|
global: parseScopeProvenance(files.get(globalMeta), globalMeta),
|
|
248
355
|
cwd: parseScopeProvenance(files.get(cwdMeta), cwdMeta),
|
|
249
|
-
session: streams.session === undefined ? {} : parseArtifactProvenanceRegistry(existingRuntime?.meta.artifacts, "State Flow session artifact provenance"),
|
|
356
|
+
session: copy ? structuredClone(copy.provenance) : streams.session === undefined ? {} : parseArtifactProvenanceRegistry(existingRuntime?.meta.artifacts, "State Flow session artifact provenance"),
|
|
250
357
|
};
|
|
251
358
|
if (expectedShared && (["global", "cwd"] as const).some((scope) => !sameJson(candidate.read(0, scope), expectedShared[scope]))) {
|
|
252
359
|
throw new Error("Legacy branch shared scopes diverged from the selected revision; migration cannot overwrite them");
|
|
253
360
|
}
|
|
254
|
-
const publication = candidate.publish(snapshot, true);
|
|
361
|
+
const publication = copy ? candidate.publishForkOrigin(snapshot) : candidate.publish(snapshot, true);
|
|
255
362
|
this.view = candidate.view;
|
|
256
363
|
this.base = candidate.base;
|
|
257
364
|
this.backend = backend;
|
|
258
365
|
this.provenanceByScope = structuredClone(candidate.provenanceByScope);
|
|
259
366
|
this.semanticRevision = candidate.semanticRevision;
|
|
260
367
|
this.savedRuntime = candidate.savedRuntime;
|
|
368
|
+
this.absentSharedScopes.clear();
|
|
369
|
+
return publication;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Initial copy owns only the new session files, without pruning or rewriting shared provenance. */
|
|
373
|
+
private publishForkOrigin(snapshot: Snapshot): RuntimePublication {
|
|
374
|
+
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, this.view!.lineage, this.backend === "files" ? "files" : "unconfirmed", this.provenanceByScope.session);
|
|
375
|
+
const result = this.backend === "files"
|
|
376
|
+
? publishTemporalStateToFiles(this.cwd, this.sessionId, this.view!, ["session"], this.base!, this.root, runtime, this.sessionKey, this.provenanceByScope)
|
|
377
|
+
: publishTemporalStateToGit(this.cwd, this.sessionId, this.view!, ["session"], this.base!, this.root, runtime, this.sessionKey, (snapshot.meta.remotePublication?.mode ?? "transition") === "transition", this.provenanceByScope);
|
|
378
|
+
const publication: RuntimePublication = result;
|
|
379
|
+
this.base = publication.base;
|
|
380
|
+
this.semanticRevision = publication.revision ?? publication.commit;
|
|
381
|
+
this.savedRuntime = hashJson(runtime);
|
|
261
382
|
return publication;
|
|
262
383
|
}
|
|
263
384
|
|
|
@@ -295,10 +416,23 @@ export class TemporalRuntime {
|
|
|
295
416
|
const provenance = structuredClone(this.provenanceByScope);
|
|
296
417
|
const adopted = new Map<StateScope, ScopeStream>();
|
|
297
418
|
const targets: StateScope[] = [];
|
|
419
|
+
const removedTargets: StateScope[] = [];
|
|
420
|
+
const absentScopes: StateScope[] = [];
|
|
421
|
+
const head = "head" in captured ? captured.head : undefined;
|
|
422
|
+
const reconciliation = `${head ?? "files"}:reconcile:${randomUUID()}`;
|
|
298
423
|
for (const scope of SHARED_SCOPES) {
|
|
299
424
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
425
|
+
const presence = classifyScopeStream(liveFiles.get(paths.checkpoint)?.content, liveFiles.get(paths.patches)?.content, scope, scope === "cwd" ? this.cwd : undefined);
|
|
426
|
+
if (presence.kind === "absent") {
|
|
427
|
+
absentScopes.push(scope);
|
|
428
|
+
provenance[scope] = {};
|
|
429
|
+
if (changedScopes.has(scope) && !this.absentSharedScopes.has(scope)) removedTargets.push(scope);
|
|
430
|
+
if (!this.absentSharedScopes.has(scope)) {
|
|
431
|
+
adopted.set(scope, freshEmptyScopeStream(scope, `${reconciliation}:${scope}:absent`));
|
|
432
|
+
}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const stream = presence.stream;
|
|
302
436
|
const liveProvenance = parseScopeProvenance(liveFiles.get(paths.meta)?.content, paths.meta);
|
|
303
437
|
const streamDrifted = !sameJson(stream, this.view!.scopes[scope]);
|
|
304
438
|
const provenanceDrifted = !sameJson(liveProvenance, this.provenanceByScope[scope]);
|
|
@@ -310,19 +444,20 @@ export class TemporalRuntime {
|
|
|
310
444
|
provenance[scope] = liveProvenance;
|
|
311
445
|
if (streamDrifted) adopted.set(scope, stream);
|
|
312
446
|
}
|
|
313
|
-
|
|
314
|
-
if (adopted.size === 0) return { view: this.view!, base: captured, provenance };
|
|
315
|
-
const streams = {
|
|
447
|
+
const reconciledView = () => adopted.size === 0 ? this.view! : adoptTemporalStreams({
|
|
316
448
|
global: adopted.get("global") ?? structuredClone(this.view!.scopes.global),
|
|
317
449
|
cwd: adopted.get("cwd") ?? structuredClone(this.view!.scopes.cwd),
|
|
318
450
|
session: structuredClone(this.view!.scopes.session),
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
451
|
+
}, reconciliation);
|
|
452
|
+
if (removedTargets.length > 0) {
|
|
453
|
+
this.view = reconciledView();
|
|
454
|
+
this.base = captured;
|
|
455
|
+
this.provenanceByScope = provenance;
|
|
456
|
+
for (const scope of absentScopes) this.absentSharedScopes.add(scope);
|
|
457
|
+
throw removedTargetScopeConflict(removedTargets);
|
|
458
|
+
}
|
|
459
|
+
if (targets.length > 0) throw targetScopeConflict(targets);
|
|
460
|
+
return { view: reconciledView(), base: captured, provenance };
|
|
326
461
|
}
|
|
327
462
|
|
|
328
463
|
publish(
|
|
@@ -372,6 +507,7 @@ export class TemporalRuntime {
|
|
|
372
507
|
this.provenanceByScope = nextProvenance;
|
|
373
508
|
this.savedRuntime = fingerprint;
|
|
374
509
|
this.semanticRevision = result.revision;
|
|
510
|
+
if (semantic) this.absentSharedScopes.clear();
|
|
375
511
|
return { base: result.base, revision: result.revision };
|
|
376
512
|
}
|
|
377
513
|
if (!scopedWrite) {
|
|
@@ -395,6 +531,7 @@ export class TemporalRuntime {
|
|
|
395
531
|
this.provenanceByScope = nextProvenance;
|
|
396
532
|
this.savedRuntime = fingerprint;
|
|
397
533
|
if (scopedWrite && result.commit) this.semanticRevision = result.commit;
|
|
534
|
+
if (semantic) this.absentSharedScopes.clear();
|
|
398
535
|
return result;
|
|
399
536
|
}
|
|
400
537
|
}
|
|
@@ -57,16 +57,11 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
|
|
|
57
57
|
diagnostics.recent,
|
|
58
58
|
);
|
|
59
59
|
const available = diagnostics.temporal !== undefined && diagnostics.durableStateError === undefined;
|
|
60
|
-
const materialized = !available ? undefined :
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
diagnostics.scopeStates.global,
|
|
66
|
-
diagnostics.scopeStates.cwd,
|
|
67
|
-
diagnostics.scopeStates.session,
|
|
68
|
-
),
|
|
69
|
-
};
|
|
60
|
+
const materialized = !available ? undefined : overlayStates(
|
|
61
|
+
diagnostics.scopeStates.global,
|
|
62
|
+
diagnostics.scopeStates.cwd,
|
|
63
|
+
diagnostics.scopeStates.session,
|
|
64
|
+
);
|
|
70
65
|
const stateJson = materialized === undefined ? undefined : JSON.stringify(materialized, null, 2);
|
|
71
66
|
const freshnessError = diagnostics.artifactFreshnessError ?? (available ? undefined : "temporal global artifact registry is unavailable");
|
|
72
67
|
const stale = freshnessError === undefined
|
|
@@ -124,7 +119,7 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
|
|
|
124
119
|
`Publication: ${publication}`,
|
|
125
120
|
...staleLines,
|
|
126
121
|
...(stateJson === undefined
|
|
127
|
-
? ["
|
|
128
|
-
: [`
|
|
122
|
+
? ["Effective memory: unavailable"]
|
|
123
|
+
: [`Effective memory (${Buffer.byteLength(stateJson, "utf8")} JSON bytes; global → CWD → session overlay):`, "", stateJson]),
|
|
129
124
|
].join("\n");
|
|
130
125
|
}
|
|
@@ -90,7 +90,8 @@ export function planTemporalPublication(
|
|
|
90
90
|
changedScopes.push(scope);
|
|
91
91
|
}
|
|
92
92
|
const provenanceUpdates: OwnedFileUpdate[] = [];
|
|
93
|
-
|
|
93
|
+
// Shared provenance belongs to its semantic scopes, not the session config being saved.
|
|
94
|
+
if (!runtimeOnly && provenance !== undefined) {
|
|
94
95
|
for (const scope of ["global", "cwd"] as const) {
|
|
95
96
|
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
96
97
|
const registry = provenance[scope];
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ORDINARY_ARTIFACT_COMPILER,
|
|
4
4
|
validateArtifactMetadata,
|
|
5
5
|
validateArtifactRegistry,
|
|
6
|
+
validateModelArtifactPatch,
|
|
6
7
|
type ArtifactCompilerOutput,
|
|
7
8
|
type ArtifactProvenance,
|
|
8
9
|
} from "./artifact.ts";
|
|
@@ -77,9 +78,6 @@ function compileReadSkills(
|
|
|
77
78
|
if (!isObject(output)) {
|
|
78
79
|
throw new Error(`Every successfully read Skill must have a CWD artifact compiler output at artifacts[exactReadPath]; missing: ${read.path}`);
|
|
79
80
|
}
|
|
80
|
-
if (Object.hasOwn(output, "hash") || Object.hasOwn(output, "compiler")) {
|
|
81
|
-
throw new Error(`Skill artifact compiler output at ${read.path} cannot set runtime-owned hash or compiler fields`);
|
|
82
|
-
}
|
|
83
81
|
if (typeof output.description !== "string" || output.description.trim().length === 0) {
|
|
84
82
|
throw new Error(`Skill artifact compiler output at ${read.path} must have a non-empty description`);
|
|
85
83
|
}
|
|
@@ -133,6 +131,7 @@ function validateScopePatch(scope: unknown, patch: unknown): asserts patch is Sc
|
|
|
133
131
|
throw new Error(`Scoped State Flow patch field ${key} must be a JSON object`);
|
|
134
132
|
}
|
|
135
133
|
}
|
|
134
|
+
if (isObject(patch.artifacts)) validateModelArtifactPatch(patch.artifacts);
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
function completePatch(patch: ScopePatch, response: string): StatePatch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-state-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Incremental scoped state/context compiler for Pi, inspired by SKILL.state",
|
|
6
6
|
"keywords": [
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"check": "node --experimental-strip-types -e \"await import('./index.ts')\"",
|
|
32
32
|
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
33
|
+
"benchmark": "node --experimental-strip-types benchmarks/benchmark.ts",
|
|
33
34
|
"typecheck": "tsc --noEmit",
|
|
34
35
|
"validate": "npm run typecheck && npm test && npm run check",
|
|
35
36
|
"prepack": "npm run validate"
|
|
@@ -50,9 +51,9 @@
|
|
|
50
51
|
"node": ">=22.19.0"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "^0.84.4",
|
|
54
|
-
"@earendil-works/pi-ai": "^0.84.4",
|
|
55
|
-
"@earendil-works/pi-coding-agent": "^0.84.4"
|
|
54
|
+
"@earendil-works/pi-agent-core": "^0.84.4 || ^0.85.1",
|
|
55
|
+
"@earendil-works/pi-ai": "^0.84.4 || ^0.85.1",
|
|
56
|
+
"@earendil-works/pi-coding-agent": "^0.84.4 || ^0.85.1"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/node": "latest",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@llblab/pi-clean-room": "0.1.1",
|
|
45
45
|
"@llblab/pi-codex-usage": "0.9.4",
|
|
46
46
|
"@llblab/pi-grow-loop": "0.8.1",
|
|
47
|
-
"@llblab/pi-state-flow": "0.
|
|
47
|
+
"@llblab/pi-state-flow": "0.10.1",
|
|
48
48
|
"@llblab/pi-telegram": "0.45.8",
|
|
49
49
|
"@llblab/skills": "1.15.0"
|
|
50
50
|
},
|