@llblab/pi-kit 0.10.1 → 0.10.2

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.10.2 - 2026-09-11
6
+
7
+ - `State Flow Publication`: Advances the exact State Flow pin to `0.9.3`, syncing the publication-queue lineage-rewrite hotfix. A queued target orphaned by a reset or re-initialized journal now retargets the live lineage instead of failing every `turn_end`, and deferred remote publication is reported accurately. The package set, resource inventory, and explicit load order remain unchanged.
8
+
5
9
  ## 0.10.1 - 2026-09-11
6
10
 
7
11
  - `State Flow Telegram Surface`: Advances the exact State Flow pin to `0.9.2`, so State Flow appears in Telegram exactly once: the pre-Settings section button carries the live status (`🌀 State Flow: #N`, `⚫️ State Flow` while off) and the duplicated Status-screen row is removed. 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.9.2` | Atomic scoped state cohorts with final-eligibility resolution |
17
+ | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.9.3` | Atomic scoped state cohorts with final-eligibility resolution |
18
18
  | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.5` | 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
 
@@ -22,7 +22,7 @@
22
22
  - 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; never use repository-wide staging. 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
23
  - 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. 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.
24
24
  - Bind every loaded scope and active Pi branch/checkpoint to the corresponding State Flow Git revision. Resume and tree restoration must recover branch-correct runtime config and semantic layers from that revision without blindly importing Git `HEAD`; reading an older revision must use object-level Git reads and never reset or check out the shared repository worktree.
25
- - Every accepted semantic change, including session-only and response-only changes, atomically updates affected checkpoint/tail pairs and temporal metadata and creates one immediate local Git commit when Git is available; each commit stages the complete non-ignored worktree delta (`git add -A` semantics, including manual deletions) before overlaying exact prepared State Flow outputs, respects `.gitignore`, keeps State Flow-owned active files under compare-and-swap protection, and synchronizes the caller-visible index to the committed tree. `remotePublication` is branch runtime policy: `turn-end` queues only the newest accepted target for asynchronous non-interactive push, `off` remains local-only, and `transition` preserves synchronous legacy behavior. Queue state is operational metadata beneath the Git common directory, not semantic history; use exact targets, destination identity, symlink-safe atomic CAS, descendant-only coalescing, cross-process worker leases, restart recovery, and truthful failure diagnostics. Remote failure never rolls back accepted state or regenerates an answer. A repository with no remote is intentionally local-only; only Git executable `ENOENT` authorizes file-only mode.
25
+ - Every accepted semantic change, including session-only and response-only changes, atomically updates affected checkpoint/tail pairs and temporal metadata and creates one immediate local Git commit when Git is available; each commit stages the complete non-ignored worktree delta (`git add -A` semantics, including manual deletions) before overlaying exact prepared State Flow outputs, respects `.gitignore`, keeps State Flow-owned active files under compare-and-swap protection, and synchronizes the caller-visible index to the committed tree. `remotePublication` is branch runtime policy: `turn-end` queues only the newest accepted target for asynchronous non-interactive push, `off` remains local-only, and `transition` preserves synchronous legacy behavior. Queue state is operational metadata beneath the Git common directory, not semantic history; use exact targets, destination identity, symlink-safe atomic CAS, descendant coalescing with lineage-rewrite recovery that retargets the live commit, cross-process worker leases, restart recovery, and truthful failure diagnostics. Remote failure never rolls back accepted state or regenerates an answer. A repository with no remote is intentionally local-only; only Git executable `ENOENT` authorizes file-only mode.
26
26
  - Register `patch_state` as the sole model-authored semantic mutation protocol. It accepts optional fixed `global`, `cwd`, and `session` semantic patches plus optional `final:true`; require at least one scope or `final:true`, reject unknown fields, `final:false`, empty supplied scopes, material no-ops, and the retired `{scope, patch}` / `unchanged` grammar. Validate every supplied scope against one causal basis and publish it all-or-nothing with one identity, temporal boundary, and durable cohort. Never accept model-authored `response`.
27
27
  - Treat every semantic `patch_state` as a strict inference barrier. Give it sequential execution mode, inspect Pi's synchronized current assistant message during tool preflight, require exactly one `patch_state` call in that response, and block every sibling tool call before execution. Every enabled iteration begins terminal-ineligible; only a successful call containing `final:true` latches eligibility for the next accepted `turn_end`, without stopping later reasoning, tools, or patches. If a terminal draft ends before eligibility, preserve it as the runtime-owned response at that `turn_end` and start at most two same-run fallback turns whose only purpose is the `final:true` patch; an eligible draft whose final validation fails after a later acquisition follows the same path. Fallback turns never enter `response`, and their only instruction is to apply `patch_state` with any durable changes and `final:true`, or `{final:true}` alone. A successful fallback closes resolution with the preserved answer intact; two failed fallbacks close the iteration with the preserved answer and current state plus one bounded warning and a finalization diagnostic. Failed patch calls do not consume fallback turns. A subsequent legal patch remains possible, and only an accepted ordinary answer is reconciled.
28
28
  - Reconcile `response` only from the actually accepted ordinary assistant answer at `turn_end`; it remains runtime-owned. State Flow has no terminal HTML-comment mutation protocol and does not parse generic service comments. The first terminal draft that cannot be reconciled is preserved as the iteration response rather than discarded; fallback turns during pending resolution never reach `response`. Other extensions retain ownership of their own comments and output handling.
@@ -2,6 +2,11 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.9.3: Publication queue lineage-rewrite hotfix
6
+
7
+ - `Publication`: Retarget a queued target orphaned by a journal reset or re-initialization to the live lineage and keep publishing, instead of failing every `turn_end` with "Publication queue targets diverge"; the retired commit stays in local Git history.
8
+ - `Diagnostics`: Record the retired target in opt-in diagnostics and report a deferred remote publication accurately instead of claiming the final response could not be reconciled.
9
+
5
10
  ## 0.9.2: Single Telegram surface
6
11
 
7
12
  - `Interop`: State Flow now appears on Telegram exactly once. The inline main-menu section button before Settings carries the live status (`🌀 State Flow: #N`, `⚫️ State Flow` while off) and opens the existing start/stop submenu; the duplicated Status-screen row is removed, so the same value no longer renders in two places. Fail-open registration and shutdown disposal are unchanged.
@@ -175,7 +175,7 @@ Session `config.json` owns runtime behavior such as `enabled`. Session `meta.jso
175
175
 
176
176
  Git-backed Pi checkpoint data is exactly `{"revision":"<full commit hash>"}`; file-only checkpoints use `{"revision":"file:<64 lowercase hex>"}` for an exact current-cohort reference. `{"disabled":true}` denotes an ordinary disabled branch without durable runtime. Configuration, counters, specification, lineage, and semantic state are not copied into these entries. Older config/meta and state-bearing checkpoints remain read-only compatibility input. Invalid immutable pointer targets fall back through the selected branch; transient publication-lock failure instead retains the selected revision for retry. Explicit start on a pre-runtime branch establishes an empty session origin, even if a later branch already created same-session files. Shared streams remain unchanged and the later session state remains recoverable from its Git revision; it is not imported into the new origin. Legacy checkpoint syntax and immutable targets undergo the same fallback checks, while an unanchored legacy semantic payload cannot be silently reduced to an ordinary-disabled marker. Stop on an unproven branch fails visibly instead of turning failed recovery into permission to replace existing runtime; select a valid checkpoint before retrying.
177
177
 
178
- Every materially effective semantic transition, including response-only and session-only changes, immediately publishes affected checkpoint/tail pairs and necessary lineage metadata. In Git mode each accepted cohort gets its own consistent local commit; `turn-end` queues the newest target for asynchronous replication, `off` stays local, and compatibility `transition` attempts the push synchronously. Intermediate patches are not batched until turn end. With no remote configured, Git persistence is intentionally local-only, not a pending publication error. With Git absent, persistence remains file-only and does not report fake commits or pending pushes. One model-authored call may update global, CWD, and session together; every supplied scope shares one validation basis, transition identity, temporal boundary, and durable commit. Config-only changes may persist runtime state but never invent semantic transitions.
178
+ Every materially effective semantic transition, including response-only and session-only changes, immediately publishes affected checkpoint/tail pairs and necessary lineage metadata. In Git mode each accepted cohort gets its own consistent local commit; `turn-end` queues the newest target for asynchronous replication, `off` stays local, and compatibility `transition` attempts the push synchronously. A reset or re-initialized journal rewrites the lineage; the queue then retargets the live commit and keeps the retired one in local history instead of blocking later turns. Intermediate patches are not batched until turn end. With no remote configured, Git persistence is intentionally local-only, not a pending publication error. With Git absent, persistence remains file-only and does not report fake commits or pending pushes. One model-authored call may update global, CWD, and session together; every supplied scope shares one validation basis, transition identity, temporal boundary, and durable commit. Config-only changes may persist runtime state but never invent semantic transitions.
179
179
 
180
180
  Writes use regular non-symlink owned files, same-directory atomic rename, isolated Git indexes, and compare-and-swap against the reconciled base. Cooperating State Flow Git publishers hold a common-Git-directory publication lock through capture, commit, and rollback. Raw bytes own file identity and recovery; non-UTF-8 explanatory journals are not reconstructed from decoded strings. Per-file publication checks catch changed bases. Both semantic and migration publishers retain exact prepared-output receipts: Git commits those bytes, not later worktree contents, and rollback requires the receipt to preserve detected external changes rather than replacing them. Low-level file helpers require caller exclusion; this is not kernel-atomic multi-file CAS against writers ignoring the protocol. An existing publication lock fails before writes and is not silently stolen; reconcile its active or interrupted owner before retrying. Scope keys mirror Pi's CWD/session directory names and retain separately verified canonical identity provenance inside owned state. Conflicts fail rather than silently selecting another scope or auto-merging semantic state. Every Git commit stages the complete non-ignored worktree delta — tracked edits, new files, and manual deletions, respecting `.gitignore` — then overlays the exact prepared State Flow outputs and synchronizes the caller-visible index to the committed tree, so no artificial staged or unstaged entries remain; unrelated user worktree changes may therefore be committed while ignored files stay untouched. With distinct roots, the Knowledge repository's files, index, HEAD, locks, and remote are not used for state publication; keep custom stores separate from sources to retain this isolation.
181
181
 
@@ -132,7 +132,7 @@ The persisted `remotePublication` policy is:
132
132
 
133
133
  A destination is identified by canonical Git common directory, remote and full ref. Queue files live beneath the Git common directory and are not semantic history.
134
134
 
135
- The queue uses exact commit targets, strict versioned JSON, symlink-safe atomic writes, CAS receipts and exclusive writer locks. A proven descendant may supersede an older target; divergent targets and changed destinations fail closed.
135
+ The queue uses exact commit targets, strict versioned JSON, symlink-safe atomic writes, CAS receipts and exclusive writer locks. A proven descendant may supersede an older target; a journal lineage rewrite retargets the live commit and records the retired target, while changed destinations fail closed.
136
136
 
137
137
  After accepted response reconciliation, an asynchronous non-interactive worker pushes the newest target. Queue failure never rolls back semantic state or regenerates an answer. Failed and interrupted attempts remain retryable across restart. Destination-scoped worker leases prevent cross-process overlap, preserve live owners, recover proven-dead owners and use exact release tokens. Confirmation removes only the exact completed target; a newer descendant remains queued.
138
138
 
@@ -252,7 +252,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
252
252
  if (mode !== "turn-end" || target === undefined || !/^[0-9a-f]{40,64}$/.test(target)) return;
253
253
  turnPublicationTarget = target;
254
254
  try {
255
- enqueueTurnPublication();
255
+ enqueueTurnPublication(ctx);
256
256
  launchPublicationWorker();
257
257
  } catch (error) {
258
258
  ctx.ui.notify(
@@ -288,7 +288,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
288
288
  return true;
289
289
  }
290
290
 
291
- function enqueueTurnPublication(): void {
291
+ function enqueueTurnPublication(ctx: ExtensionContext): void {
292
292
  const target = turnPublicationTarget;
293
293
  turnPublicationTarget = undefined;
294
294
  if (!target) return;
@@ -297,7 +297,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
297
297
  const path = publicationQueuePath(destination);
298
298
  const previous = loadPublicationQueue(path);
299
299
  const next = previous
300
- ? coalescePublicationTarget(previous, destination, target, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant))
300
+ ? coalescePublicationTarget(previous, destination, target, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant), {
301
+ onDivergedLineage: (dropped) => recordDiagnostic(
302
+ `Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`,
303
+ "publication-conflict", ctx,
304
+ ),
305
+ })
301
306
  : createPublicationQueue(destination, target);
302
307
  savePublicationQueue(path, next, previous);
303
308
  }
@@ -365,7 +370,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
365
370
  if (mode === "turn-end") {
366
371
  turnPublicationTarget = target;
367
372
  try {
368
- enqueueTurnPublication();
373
+ enqueueTurnPublication(ctx);
369
374
  launchPublicationWorker();
370
375
  } catch (error) {
371
376
  ctx.ui.notify(`State Flow retained local state; asynchronous publication recovery is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
@@ -1035,17 +1040,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1035
1040
  updateUi(ctx);
1036
1041
  return;
1037
1042
  }
1043
+ let responseCommitted = false;
1038
1044
  try {
1039
1045
  const response = finalizedAssistantResponse(event.message);
1040
1046
  const stage = stageScopedTransition(scopeStates, { transitions: [], response }, [], runtime!.causalBasis());
1041
1047
  commitStage(stage, ctx, true);
1048
+ responseCommitted = true;
1042
1049
  bootstrapContinuation = undefined;
1043
1050
  rehydrationPhase = "step";
1044
- enqueueTurnPublication();
1051
+ enqueueTurnPublication(ctx);
1045
1052
  if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
1046
1053
  } catch (error) {
1047
1054
  recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
1048
- ctx.ui.notify(`State Flow could not reconcile the final response: ${error instanceof Error ? error.message : String(error)}`, "error");
1055
+ ctx.ui.notify(responseCommitted
1056
+ ? `State Flow committed the final response; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`
1057
+ : `State Flow could not reconcile the final response: ${error instanceof Error ? error.message : String(error)}`, "error");
1049
1058
  } finally {
1050
1059
  responseAwaitingReconciliation = false;
1051
1060
  }
@@ -81,13 +81,26 @@ export function createPublicationQueue(destination: RemotePublicationDestination
81
81
  return { version: 1, destination: structuredClone(destination), target, status: "pending", attempt: 0 };
82
82
  }
83
83
 
84
- export function coalescePublicationTarget(state: PublicationQueueState, destination: RemotePublicationDestination, target: string, isAncestor: CommitAncestor): PublicationQueueState {
84
+ export interface PublicationCoalesceObserver {
85
+ /** Called with the retired record when a journal lineage rewrite orphans its target. */
86
+ onDivergedLineage?: (previous: PublicationQueueState) => void;
87
+ }
88
+
89
+ export function coalescePublicationTarget(state: PublicationQueueState, destination: RemotePublicationDestination, target: string, isAncestor: CommitAncestor, observer?: PublicationCoalesceObserver): PublicationQueueState {
85
90
  validatePublicationQueue(state);
86
91
  if (!COMMIT.test(target)) throw new Error("Publication queue target must be an exact commit");
87
92
  if (remotePublicationDestinationKey(state.destination) !== remotePublicationDestinationKey(destination)) throw new Error("Publication queue destination changed");
88
93
  if (target === state.target || isAncestor(target, state.target)) return structuredClone(state);
89
- if (!isAncestor(state.target, target)) throw new Error("Publication queue targets diverge");
90
- return { ...structuredClone(state), target, status: "pending", attempt: 0, error: undefined };
94
+ const previous = structuredClone(state);
95
+ if (!isAncestor(state.target, target)) {
96
+ // A reset or re-initialized journal rewrites the lineage. The queued commit can never
97
+ // fast-forward the remote again, so retarget the live lineage instead of wedging every
98
+ // later turn-end; the retired commit stays in the local Git object store.
99
+ observer?.onDivergedLineage?.(previous);
100
+ const { confirmed: _confirmed, error: _error, ...live } = previous;
101
+ return { ...live, target, status: "pending", attempt: 0 };
102
+ }
103
+ return { ...previous, target, status: "pending", attempt: 0, error: undefined };
91
104
  }
92
105
 
93
106
  export function parsePublicationQueue(content: string): PublicationQueueState {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
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.10.1",
3
+ "version": "0.10.2",
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.9.2",
47
+ "@llblab/pi-state-flow": "0.9.3",
48
48
  "@llblab/pi-telegram": "0.45.5",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },