@llblab/pi-kit 0.11.0 → 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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.11.1 - 2026-09-13
6
+
7
+ - `State Flow Filesystem Recovery`: Advances the exact State Flow pin to `0.10.1`, adding transactional self-healing when a complete live global or CWD checkpoint pair disappears, exact private-session reconstruction from retained Git evidence, and precise fail-closed handling for partial or unsafe storage loss. The package set, resource inventory, and explicit load order remain unchanged.
8
+
5
9
  ## 0.11.0 - 2026-09-13
6
10
 
7
11
  - `State Flow Maturation`: Advances the exact State Flow pin to `0.10.0`, adding safe native compaction of sufficiently large completed histories, effective-memory-only status JSON, stronger fork/recovery and concurrent publication guarantees, and lower repeated Git/session restoration work while preserving full Pi session traces and exact state lineage. The package set, resource inventory, and explicit load order remain unchanged.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
14
14
  | [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.1.1` | Isolated nested Pi TUI with explicitly selected extensions |
15
15
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
16
16
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
17
- | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.10.0` | Durable scoped state with native session compaction and exact publication |
17
+ | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.10.1` | Durable scoped state with native session compaction and exact publication |
18
18
  | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.8` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
19
19
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
20
20
 
@@ -18,6 +18,7 @@
18
18
  - Define `state[n]` and `state.global[n]`, `state.cwd[n]`, `state.session[n]` at the same nth previous accepted transition boundary in the active lineage, never the nth local patch of each scope. Guarantee offsets zero through seven once the lineage has seven proven transitions; report earlier-than-origin history as unavailable for new/migrated lineages and reject offsets beyond seven on the hot interface. Reconstruct scopes at one target before overlaying them.
19
19
  - Keep historical reads lazy through the smallest runtime/model read interface. Normal inference gets only current effective state and useful bounded compact transition context, never eight full snapshots. `patch_state` is the sole semantic mutation tool. `read_state` exposes one cached effective/global/CWD/session projection at offset 0–7, defaulting to effective/0; it returns the exact boundary with semantic state and never publishes, appends a checkpoint, or advances history. Both tools follow branch enablement and host tool restrictions; the patch barrier blocks reader siblings too. Live/cached checkpoint plus tails own the hot path; use Git for branch restoration and explicit cold inspection, not to rebuild current state on every inference.
20
20
  - Store runtime state in its own directory with optional Git durability, defaulting to `state-flow/` beneath Pi's configured agent directory (`~/.pi/agent/state-flow/` normally), independently from the Knowledge Markdown source root. Use exactly these owned paths: `checkpoint.json`, `patches.jsonl`, and `meta.json` for global; `<cwd-key>/checkpoint.json`, `<cwd-key>/patches.jsonl`, and `<cwd-key>/meta.json` for CWD; `<cwd-key>/<session-key>/checkpoint.json`, `patches.jsonl`, `config.json`, and `meta.json` for session. Do not create `.state-flow`, `scopes`, or another storage/history namespace. Mirror Pi's native CWD session-directory encoding and JSONL-basename session key (deriving `<header timestamp>_<UUID>` for in-memory sessions), while retaining separately verifiable canonical identity provenance in owned state. Keep the UUID authoritative, reject unsafe segments and mismatches rather than selecting another scope, and use CWD checkpoint ownership to fail closed on Pi-name collisions.
21
+ - Classify every filesystem cohort before recovery: complete valid evidence is usable; total absence is recoverable only from an explicit semantic default or exact surviving authority; partial, malformed, contradictory, or authority-losing evidence fails closed at the smallest dependent capability. A wholly absent untouched global/CWD checkpoint-tail pair is current empty shared reality and must not resurrect selected cold values; a patch targeting that disappeared scope is stale and must fail for reinference. Missing provenance means unavailable freshness, not semantic corruption. Perform every repair through normal locked/CAS publication, never ad-hoc writes.
21
22
  - Migrate legacy current `state.json` losslessly into an initial anchored checkpoint with an empty tail; current snapshots are the sole semantic recovery basis, independent of the presence or validity of explanatory journals, including historical revision reads. Preserve only proven history, remove obsolete files within the successful scoped migration, and never retain two authoritative formats. Exercise migrations in temporary repositories rather than modifying the user's active Knowledge data during development.
22
23
  - Read and write only regular non-symlink State Flow-owned files at those exact paths, publish each file by same-directory atomic rename, preserve arbitrary repository contents, and classify ownership exactly. Git acceptance follows the complete-delta isolated-index contract below. Retain opaque source bytes for file identity and rollback, not decoded-text reconstructions. Use validated structural JSON equality for in-process semantic comparisons; reserve cryptographic hashes for compact identities that cross inference, persistence, process, or source-freshness boundaries. Serialize cooperating Git publications through a common-Git-directory publication lock; low-level file helpers require caller exclusion. Recheck prepared bases before each replacement/deletion and restore only bytes still matching the publisher's own output. Preserve detected concurrent changes and report unresolved rollback conflicts; do not claim kernel-atomic multi-file CAS against nonparticipating writers. Markdown discovery uses its independently configured source root, defaulting to `knowledge/` beneath Pi's agent directory; a storage-root override must not redirect source discovery.
23
24
  - At instance initialization resolve the repository, CWD key, and session key; load each scope's anchored checkpoint and tail, materialize at the selected temporal boundary, then overlay `global → cwd → session`. Install cached view and publication basis atomically only after successful restoration/initialization; unavailable publication is an error, not a semantic no-op. Branch recovery may reuse one owner-bound, single-use inspection of the exact selected immutable Git revision, with a detached inspection snapshot and a freshly acquired live publication basis at installation. Revalidate mutable file cohorts, legacy snapshot fallbacks, and redirected runtime owners; never turn inspection into a long-lived revision or publication-basis cache. Preserve the selected revision across transient restore failure so an explicit start can retry it. A genuinely new session gets an empty session layer and inherits only global/CWD values; it must never reuse another same-CWD session layer. Explicit native fork adoption instead follows the [session-copy contract](docs/fork-contract.md): copy the proven source session checkpoint/tail and provenance into a distinct owner over current shared streams, without pruning shared provenance or modifying the parent. Use a fresh origin, preserve copied replay records, reject occupied live/HEAD targets, and publish/checkpoint the child only after CAS acceptance. Inherited parent pointers never authorize an empty reset; fence parent passive Stop projection across child reload.
@@ -2,19 +2,12 @@
2
2
 
3
3
  Completed release work belongs in [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
- ## 0.10.0: Session performance and reliability
5
+ ## 0.10.1: Filesystem self-healing and graceful degradation
6
6
 
7
- - **Outcome:** Make State Flow complement Pi's native session lifecycle reliably and efficiently after long histories and while independent instances publish to the same store. The operator reports slowdown after resuming a large session with roughly 200 patches; concurrency is a hypothesis, not an established cause.
8
- - **Evolution:** Treat 0.10.0 as a maturation milestone: deepen reliability, usability, performance, consistency, and explanation of the existing concept. Admit adjacent maintenance only from concrete evidence; do not add features or change the product's core model.
9
- - **Execution:** Prefer inline implementation and review. If subagent delegation becomes necessary, use only the operator-requested "GPT-5.6 Luna" after verifying its exact available identity; no silent substitution. The coordinator owns review and integration. Existing deterministic benchmark processes may finish without launching more model actors.
10
- - **Convergence:** Reproduce and measure first, implement only evidenced corrections, validate each owned slice, then separately review the integrated candidate. Stop when the regressions below pass, before/after workload evidence explains the performance changes, and remaining host or environment limits are explicit. Preserve semantic history, source/runtime ownership, native trace, branch selection, and exact publication/CAS guarantees.
11
- - **Non-goals:** No new semantic mode, state-size cap, database, hidden session replacement, bulk migration, production-store repair, read-only global bootstrap, or Telegram analyzer. Preserve ordinary abort: accepted patches remain available for same-session continuation, with no new immediate remote-enqueue requirement. Test on synthetic sessions and temporary stores; do not inspect or mutate production conversation bodies or state repositories for this work. Publication remains outside preparation scope.
12
-
13
- ### Release gate
14
-
15
- The [nine-invocation measurement series and interpretation](docs/performance.md#measurement-closure) are complete; no measurement is active. `/tmp/state-flow-final-controls-CzaHqM/final-series-receipts.json` binds the actual exits and complete captures. Preserve this evidence and refresh only claims invalidated by an admitted correction; source/workload changes must not silently inherit old timings. Original production slowdown attribution remains unproven, and production-store inspection is not authorized.
16
-
17
- The 0.10.0 candidate now includes native completed-history compaction and effective-only status JSON. Full 0.84.4 validation passes 426/426; affected policy/status/native lifecycle tests and typecheck pass on 0.85.1. The operator intentionally aligned package/lock versions to 0.10.0 and explicitly authorized commit, push and release. Release automation/remote CI and published artifacts remain the external acceptance gates.
7
+ - **Outcome:** Treat safely interpretable absence as recoverable or optional while keeping partial, malformed, contradictory, and authority-losing evidence fail-closed at the smallest affected capability. [`docs/filesystem-recovery.md`](docs/filesystem-recovery.md) owns the durable cohort classification.
8
+ - **Accepted candidate:** Shared Git/file repair, exact session authority, provenance/external degradation, operational persistence, concurrent CAS and native Pi response/reload/resume witnesses are complete. A focused release review found and corrected repeated stale-target refusal; the runtime now refreshes the empty basis after one conflict and accepts a later patch through ordinary publication.
9
+ - **Validation:** The complete 0.84.4 repository profile passes 442/442 with typecheck/import-check. On the supported 0.85.1 stack, all 16 affected codec/runtime witnesses, the real Pi lifecycle witness and typecheck pass. Package 0.10.1 contains 46 files including the recovery guide and excludes tests/benchmarks; context/DAG validation report zero errors.
10
+ - [ ] **Release gate:** Commit, push, tag, npm/GitHub publication, and any Pi Kit pin update require separate explicit release authorization.
18
11
 
19
12
  ## Candidate evolution
20
13
 
@@ -22,7 +15,7 @@ The 0.10.0 candidate now includes native completed-history compaction and effect
22
15
  - Boundary: project the existing durable global materialization only. No writes, transitions, patches, barriers, session/CWD initialization, or temporal-history changes. Model tools and the full protocol remain opt-in behind explicit start, and the bootstrap adds no second semantic mode.
23
16
  - Open questions: whether deferred Markdown freshness discovery must run ahead of the first inference, token cost of a stable global prefix, behavior under untrusted project contexts, and naming distinct from the existing bootstrap-run concept.
24
17
  - Status: candidate, not scheduled. Do not start without a dedicated release contract.
25
- - [ ] **Telegram submenu state analyzer:** Extend the State Flow Telegram section with a read-only analyzer view over the same diagnostics `/state-flow-status` already reports (branch mode, temporal head and hot depth, scope keys, retained tails, artifact freshness, publication). The operator deliberately deferred this beyond the 0.9.0 control surface.
18
+ - [ ] **Pi Telegram submenu state analyzer:** Extend the State Flow Telegram section with a read-only analyzer view over the same diagnostics `/state-flow-status` already reports (branch mode, temporal head and hot depth, scope keys, retained tails, artifact freshness, publication). The operator deliberately deferred this beyond the 0.9.0 control surface.
26
19
  - Boundary: presentation only. Reuse existing status diagnostics; no new semantic mode, and never mutate state from the analyzer view.
27
20
  - Status: candidate, not scheduled. Do not start without a dedicated release contract.
28
21
 
@@ -4,6 +4,12 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.10.1: Filesystem self-healing and graceful degradation
8
+
9
+ - `Shared scopes`: A wholly absent live global/CWD checkpoint-tail pair is now distinct from partial storage. Untouched publication adopts empty current reality at a fresh origin and transactionally materializes the canonical pair without resurrecting selected values; a patch targeting the disappeared scope fails precisely for reinference. Git/file backends retain publication locks, CAS, full cold history and concurrent-writer exclusion.
10
+ - `Authority and locality`: Exact selected Git revisions can reconstruct missing private session files, while file-only loss without a retained cohort and partial session semantic/runtime pairs remain fail-closed. Missing provenance degrades freshness without invalidating semantic artifacts; malformed provenance bytes remain untouched. Existing optional config, Knowledge discovery, queue, lease, lock and external-source absence keeps its local graceful behavior.
11
+ - `Recovery evidence`: Native Pi now reconciles an ordinary answer after complete live CWD-pair deletion, then accepts another session patch and reloads/resumes the repaired state. Regression coverage also proves no shared-value resurrection, stale-target refusal, partial-pair preservation, one-owner concurrent repair, stale-capture CAS loss, exact session reconstruction, and absent/malformed operational or external-resource behavior.
12
+
7
13
  ## 0.10.0: Native session memory and reliability
8
14
 
9
15
  - `Git work`: Single-use restoration, exact-path tree reads and NUL-safe index batching cut steady Git calls 120→87 and resume 63→21 without caching live bases or weakening exact-byte/CAS guards. Selected state/runtime blobs now bypass Node's implicit 1 MiB output limit, preserving large-state answers and cold restoration. Legacy/file-cohort, mode/owner and rollback checks remain. Call savings do not establish universal latency gains.
@@ -2,6 +2,7 @@
2
2
 
3
3
  - [Usage and recovery](usage.md): Configuration, new/resumed sessions, Start/Stop, diagnostics, privacy, and storage recovery.
4
4
  - [Architecture](architecture.md): Semantic state, temporal algebra, Pi lifecycle, storage, publication, artifacts, and embedding contracts.
5
+ - [Filesystem recovery](filesystem-recovery.md): Cohort-wide absence, partial-presence, malformed-evidence, repair-authority, and transaction rules.
5
6
  - [Temporal acceptance](temporal-acceptance.md): The twenty required temporal properties and their executable witnesses.
6
7
  - [SDK compatibility](compatibility.md): Tested dependency stacks, public lifecycle seams, isolated validation, and host limits.
7
8
  - [Physical fork contract](fork-contract.md): Session-stream copying, unchanged shared memory, child ownership/origin, and tested support boundaries.
@@ -0,0 +1,35 @@
1
+ # Filesystem recovery contract
2
+
3
+ State Flow classifies absence separately from partial or malformed evidence. Recovery may derive bytes only from an authoritative surviving cohort or from a semantic default that the owner explicitly permits. It never invents history, ownership, provenance, or external success.
4
+
5
+ | Resource or cohort | Owner / authority | Total absence | Partial or malformed presence | Allowed repair and writes |
6
+ | --- | --- | --- | --- | --- |
7
+ | Global `checkpoint.json` + `patches.jsonl` | State Flow; authoritative shared semantics | Untouched publication adopts a fresh empty scope; a targeted patch conflicts | Either half missing, malformed replay, or invalid envelope fails closed | Normal CAS publication may materialize the complete empty pair |
8
+ | CWD `checkpoint.json` + `patches.jsonl` | State Flow; authoritative shared semantics with CWD identity | Same as global; selected values are not resurrected | Same as global; owner mismatch also fails closed | Normal CAS publication may materialize the complete empty pair |
9
+ | Session `checkpoint.json` + `patches.jsonl` | State Flow; authoritative private semantics | Fresh lifecycle origin may initialize; an existing selected session requires exact retained authority | Partial or malformed pair fails closed | Fresh initialization or exact selected-revision recovery only |
10
+ | Global/CWD `meta.json` | State Flow; derived artifact provenance | Provenance unavailable (`{}`); semantics remain usable | Malformed present evidence disables the dependent provenance operation | Rebuild only from fresh trusted acquisition evidence through publication |
11
+ | Session `config.json` + `meta.json` | State Flow; authoritative runtime identity, lineage and session provenance | Fresh origin may initialize; selected sessions recover only from exact authority | Partial, malformed, contradictory identity, lineage, or revision fails closed | Canonical runtime publication from proven lifecycle/selected state |
12
+ | Legacy `state.json` and legacy tails | State Flow migration input; authoritative only after codec validation | No migration input | Mixed legacy/canonical, orphan tail, malformed bytes, or identity ambiguity fails closed | Existing one-way migration machinery only |
13
+ | Selected Git revision blobs/modes | Git object database; immutable cold authority | A required blob/revision is unavailable | Mode, owner, hash, or cohort contradiction fails closed | Read-only reconstruction; never checkout/reset the live worktree |
14
+ | File-only revision pointer/cohort | State Flow/Pi entry; current exact authority only | No cold history can be invented | Any identity mismatch or incomplete retained cohort fails closed | Exact current cohort only; normal locked publication writes repairs |
15
+ | Publication queue | State Flow; operational effect intent | Empty queue / no pending publication | Malformed or contradictory bytes are preserved and publication fails locally | CAS save/remove and atomic temporary rename only |
16
+ | Worker lease | State Flow; operational ownership | Unclaimed | Malformed/foreign live evidence is preserved; live owner excludes peers | Existing dead-process reclamation protocol only |
17
+ | Publication locks | State Flow; mutual exclusion | Unlocked | Present lock excludes publishers, including interrupted owners | Current owner releases; no opportunistic deletion |
18
+ | Temporary queue files / isolated Git index | Creating State Flow operation; transient | No pending preparation | Unknown surviving files grant no authority | Creating operation cleans its own temporary path; fatal residue is not adopted |
19
+ | Extension `state-flow.json` | Operator; optional external configuration | Built-in defaults | Present unreadable/malformed/unknown settings fail extension configuration | State Flow never creates or rewrites it |
20
+ | Knowledge root and Markdown | External Knowledge owner | Freshness unavailable; durable semantic state remains | Unsafe paths or malformed/unreadable sources disable acquisition locally | Never create; semantic removal only under existing confirmed ownership rules |
21
+ | Skill and external artifact sources | External package/user owner | Freshness unavailable unless ownership proves removal semantics | Unsafe/non-regular/unreadable sources disable acquisition locally | Never create or fabricate source/provenance |
22
+ | Pi State Flow entries and diagnostics | Pi session log / State Flow entry owner | Missing optional diagnostics provide no evidence; missing required selected pointer blocks that restore | Malformed or contradictory owner/version/pointer fails the dependent restore | Append through Pi entry APIs only; no standalone diagnostics file exists |
23
+
24
+ ## Transaction rule
25
+
26
+ Every semantic repair follows the ordinary transaction path:
27
+
28
+ 1. Capture the live Git or file basis under the existing publication lock.
29
+ 2. Classify each cohort as present, absent, partial, or malformed.
30
+ 3. Derive only an authorized replacement.
31
+ 4. Stage the complete canonical cohort.
32
+ 5. Recheck CAS and ownership.
33
+ 6. Atomically publish and install the resulting runtime state.
34
+
35
+ A current wholly absent shared scope is newer live reality for an untouched transition dependency. Its replacement begins empty at a fresh reconciliation origin. If the accepted transition targets that missing scope, publication refuses the stale target and requires a later inference against the refreshed basis. Cold Git history remains inspectable but is never silently promoted back into current shared memory.
@@ -79,6 +79,14 @@ Use a dedicated directory. State storage and Knowledge Markdown have separate re
79
79
 
80
80
  Each scope materializes an anchored `checkpoint.json` plus `patches.jsonl`. Scope `meta.json` holds runtime-owned artifact evidence; the session also has `config.json` and temporal/runtime metadata. CWD/session directories mirror Pi's native naming while validating canonical identities separately. See the [storage contract](architecture.md#storage-and-identity) for the exact layout.
81
81
 
82
+ ### Missing, partial, and malformed storage
83
+
84
+ A checkpoint and tail are one semantic pair. If both live files for an untouched global or CWD scope disappear, State Flow treats that complete absence as current empty shared reality during the next accepted publication. It creates a fresh canonical pair through the normal publication lock and CAS path; selected values remain only in cold Git history and are not silently resurrected. A patch targeting the disappeared scope is rejected once as stale so a later inference can work from the actual empty basis.
85
+
86
+ Exactly one surviving pair member is corruption and fails closed. Present malformed JSON, invalid envelopes, identity contradictions, and partial session `config.json`/`meta.json` also remain fail-closed and are not replaced. A selected Git revision may reconstruct a missing private session cohort exactly; file-only mode refuses when its exact current cohort is gone because it has no cold history to invent.
87
+
88
+ Missing shared provenance means freshness evidence is unavailable while semantic state remains usable. A missing whole Knowledge root does not prove that durable artifact routing was deleted, and external files are never created. Queue, worker lease, and lock absence keep their existing meanings—empty, unclaimed, and unlocked—while malformed or foreign present evidence is preserved. See the complete [filesystem recovery contract](filesystem-recovery.md).
89
+
82
90
  ### With Git
83
91
 
84
92
  Start initializes an exact-root repository when needed, preserving existing contents. Git must have a configured commit identity. A containing ancestor repository is not a substitute. Manual-mode startup/status/restore do not initialize Git; explicit Start and automatic activation of genuinely new sessions may do so.
@@ -55,14 +55,18 @@ export function serializeScopeStream(stream: ScopeStream, scope: StateScope, cwd
55
55
  };
56
56
  }
57
57
 
58
- /** Decode the entire bounded replay input before accepting any materialized state. */
59
- export function parseScopeStream(
58
+ export type ScopeStreamPresence =
59
+ | { kind: "absent" }
60
+ | { kind: "present"; stream: ScopeStream };
61
+
62
+ /** Distinguish a wholly absent semantic cohort from partial or malformed surviving authority. */
63
+ export function classifyScopeStream(
60
64
  checkpointSource: string | undefined,
61
65
  patchesSource: string | undefined,
62
66
  scope: StateScope,
63
67
  expectedCwd?: string,
64
- ): ScopeStream | undefined {
65
- if (checkpointSource === undefined && patchesSource === undefined) return undefined;
68
+ ): ScopeStreamPresence {
69
+ if (checkpointSource === undefined && patchesSource === undefined) return { kind: "absent" };
66
70
  if (checkpointSource === undefined || patchesSource === undefined) {
67
71
  throw new Error(`State Flow ${scope} scope has an incomplete checkpoint/tail pair`);
68
72
  }
@@ -94,7 +98,18 @@ export function parseScopeStream(
94
98
  }
95
99
  const stream = { checkpoint, patches };
96
100
  validateScopeStream(stream, scope);
97
- return stream;
101
+ return { kind: "present", stream };
102
+ }
103
+
104
+ /** Decode the entire bounded replay input before accepting any materialized state. */
105
+ export function parseScopeStream(
106
+ checkpointSource: string | undefined,
107
+ patchesSource: string | undefined,
108
+ scope: StateScope,
109
+ expectedCwd?: string,
110
+ ): ScopeStream | undefined {
111
+ const presence = classifyScopeStream(checkpointSource, patchesSource, scope, expectedCwd);
112
+ return presence.kind === "present" ? presence.stream : undefined;
98
113
  }
99
114
 
100
115
  export interface TemporalScopePaths {
@@ -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";
@@ -39,6 +39,19 @@ function targetScopeConflict(scopes: readonly StateScope[]): Error {
39
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.`);
40
40
  }
41
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
+
42
55
  export class MissingSessionRuntimeError extends Error {
43
56
  constructor() { super("Linked State Flow revision has no session runtime"); }
44
57
  }
@@ -81,6 +94,8 @@ export class TemporalRuntime {
81
94
  private savedRuntime: string | undefined;
82
95
  private backend: "git" | "files" | undefined;
83
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>();
84
99
  readonly cwd: string;
85
100
  private readonly session: SessionAddress;
86
101
  readonly root: string;
@@ -119,6 +134,7 @@ export class TemporalRuntime {
119
134
  this.backend = "git";
120
135
  this.semanticRevision = result.revision;
121
136
  this.savedRuntime = savedRuntime;
137
+ this.absentSharedScopes.clear();
122
138
  return result;
123
139
  }
124
140
 
@@ -216,6 +232,7 @@ export class TemporalRuntime {
216
232
  this.provenanceByScope = structuredClone(inspected.file.provenance);
217
233
  this.semanticRevision = revision;
218
234
  this.savedRuntime = savedRuntime;
235
+ this.absentSharedScopes.clear();
219
236
  return inspected.snapshot;
220
237
  }
221
238
  if (!inspected.temporal) {
@@ -269,6 +286,7 @@ export class TemporalRuntime {
269
286
  this.provenanceByScope = structuredClone(loaded.provenance);
270
287
  this.semanticRevision = semanticRevision;
271
288
  this.savedRuntime = savedRuntime;
289
+ this.absentSharedScopes.clear();
272
290
  return resolved.snapshot;
273
291
  }
274
292
 
@@ -347,6 +365,7 @@ export class TemporalRuntime {
347
365
  this.provenanceByScope = structuredClone(candidate.provenanceByScope);
348
366
  this.semanticRevision = candidate.semanticRevision;
349
367
  this.savedRuntime = candidate.savedRuntime;
368
+ this.absentSharedScopes.clear();
350
369
  return publication;
351
370
  }
352
371
 
@@ -397,10 +416,23 @@ export class TemporalRuntime {
397
416
  const provenance = structuredClone(this.provenanceByScope);
398
417
  const adopted = new Map<StateScope, ScopeStream>();
399
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()}`;
400
423
  for (const scope of SHARED_SCOPES) {
401
424
  const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
402
- const stream = parseScopeStream(liveFiles.get(paths.checkpoint)?.content, liveFiles.get(paths.patches)?.content, scope, scope === "cwd" ? this.cwd : undefined);
403
- if (stream === undefined) throw new Error(`Live State Flow ${scope} scope storage is incomplete`);
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;
404
436
  const liveProvenance = parseScopeProvenance(liveFiles.get(paths.meta)?.content, paths.meta);
405
437
  const streamDrifted = !sameJson(stream, this.view!.scopes[scope]);
406
438
  const provenanceDrifted = !sameJson(liveProvenance, this.provenanceByScope[scope]);
@@ -412,19 +444,20 @@ export class TemporalRuntime {
412
444
  provenance[scope] = liveProvenance;
413
445
  if (streamDrifted) adopted.set(scope, stream);
414
446
  }
415
- if (targets.length > 0) throw targetScopeConflict(targets);
416
- if (adopted.size === 0) return { view: this.view!, base: captured, provenance };
417
- const streams = {
447
+ const reconciledView = () => adopted.size === 0 ? this.view! : adoptTemporalStreams({
418
448
  global: adopted.get("global") ?? structuredClone(this.view!.scopes.global),
419
449
  cwd: adopted.get("cwd") ?? structuredClone(this.view!.scopes.cwd),
420
450
  session: structuredClone(this.view!.scopes.session),
421
- };
422
- const head = "head" in captured ? captured.head : undefined;
423
- return {
424
- view: adoptTemporalStreams(streams, `${head ?? "unborn"}:reconcile:${randomUUID()}`),
425
- base: captured,
426
- provenance,
427
- };
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 };
428
461
  }
429
462
 
430
463
  publish(
@@ -474,6 +507,7 @@ export class TemporalRuntime {
474
507
  this.provenanceByScope = nextProvenance;
475
508
  this.savedRuntime = fingerprint;
476
509
  this.semanticRevision = result.revision;
510
+ if (semantic) this.absentSharedScopes.clear();
477
511
  return { base: result.base, revision: result.revision };
478
512
  }
479
513
  if (!scopedWrite) {
@@ -497,6 +531,7 @@ export class TemporalRuntime {
497
531
  this.provenanceByScope = nextProvenance;
498
532
  this.savedRuntime = fingerprint;
499
533
  if (scopedWrite && result.commit) this.semanticRevision = result.commit;
534
+ if (semantic) this.absentSharedScopes.clear();
500
535
  return result;
501
536
  }
502
537
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.10.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": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.11.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.10.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
  },