@llblab/pi-kit 0.10.0 โ†’ 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,16 @@
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
+
9
+ ## 0.10.1 - 2026-09-11
10
+
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.
12
+ - `Grow Loop Status Text`: Advances the exact Grow Loop pin to `0.8.1`, mirroring the terminal status literally as `Grow Loop โˆžN` with no waiting, running, or countdown decoration. The package set, resource inventory, and explicit load order remain unchanged.
13
+ - `Queue Refresh Icon`: Advances the exact Telegram pin to `0.45.5`, so the queue menu's Refresh button renders `๐Ÿ”„ Refresh` while `๐ŸŒ€` stays the State Flow identity. The package set, resource inventory, and explicit load order remain unchanged.
14
+
5
15
  ## 0.10.0 - 2026-09-11
6
16
 
7
17
  - `State Flow Telegram Surface`: Advances the exact State Flow pin to `0.9.0`, adding the optional fail-open Telegram status line (`State Flow: on ยท step #N`) and the inline main-menu section button before Settings with Start, Stop, Refresh, and deferred-start control for the current session branch. The package set, resource inventory, and explicit load order remain unchanged.
package/README.md CHANGED
@@ -13,9 +13,9 @@ Package links lead to the owning repositories for usage, documentation, issues,
13
13
  | [`@llblab/pi-actors`](https://github.com/llblab/pi-actors) | `0.53.0` | Inspectable local Runs, reusable Recipes, persistent tools, and delegation Skills |
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
- | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.0` | Visible continuation scheduling and bounded worker Skills |
17
- | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.9.0` | Atomic scoped state cohorts with final-eligibility resolution |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.4` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
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.3` | Atomic scoped state cohorts with final-eligibility resolution |
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
 
21
21
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
@@ -27,7 +27,7 @@
27
27
  - There is no slash-command control surface. Any ordinary user prompt exits the active runtime rhythm by clearing pending scheduling and hiding loop status; restart/continuation intent belongs to the agent and Grow Loop skill, not to a runtime latch or regex.
28
28
  - No start slash commands, budgets, cycle counts, hidden processes, or background agents.
29
29
  - Loop status is dynamic: show warning-colored `grow-loop โˆžN` only while the next iteration is deferred until idle, `grow-loop Ns` only during the configured countdown, and dim `grow-loop โˆžN` only while a loop-scheduled turn is active. Clear active status only after Pi fully settles without an armed successor; low-level run endings may still lead to retry or compaction recovery. Any user input except the runtime's exact expected continuation prompt clears pending loop scheduling and hides loop status, including operator input injected through another extension.
30
- - Telegram status mirror: when `pi-telegram` is installed, the optional fail-open status-line adapter mirrors the same visible rhythm (`Grow Loop: #N ยท waiting|Xs|running`) and hides with it. It reads the same runtime state, registers once per process lifetime, disposes on session shutdown, and a missing or unready transport changes nothing.
30
+ - Telegram status mirror: when `pi-telegram` is installed, the optional fail-open status-line adapter mirrors the same identity (`Grow Loop: โˆžN`) and hides with it. It reads the same runtime state, registers once per process lifetime, disposes on session shutdown, and a missing or unready transport changes nothing.
31
31
  - Grow Loop does not own Escape/abort semantics; Escape remains baseline Pi behavior for active agent turns. The runtime does not block future `grow_loop` tool calls; the skill contract owns whether recent user context means continue, stop, restart, or change direction.
32
32
 
33
33
  ## Topology
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1: Telegram Status Text Hotfix
4
+
5
+ - `Status Text`: The Telegram Status screen now copies the compact terminal identity exactly: `Grow Loop: โˆžN` for the armed or running iteration, without countdown, phase, or separator text. The row still hides whenever the terminal status is hidden.
6
+
3
7
  ## 0.8.0: Telegram Status Mirror
4
8
 
5
9
  - `Telegram Mirror`: With the optional `pi-telegram` extension installed, the Telegram Status screen mirrors the same loop rhythm as the terminal status and hides with it: `Grow Loop: #N ยท waiting` while the next prompt is armed until idle, `#N ยท 2.4s` during the configured countdown, and `#N ยท running` while the loop-scheduled turn is active.
@@ -150,7 +150,7 @@ The tool never blocks future calls. Whether to continue belongs to the agent and
150
150
 
151
151
  `N` is monotonic within the current extension instance and advances only once when a turn arms its deferred successor. Repeated `grow_loop` calls before that turn ends retain `N`, replace the pending delay, and report that the iteration was already scheduled. Active status clears when the scheduled agent run fully settles without arming a successor, so automatic retry or compaction recovery does not produce a false idle state. There is no `grow-loop stopped` or `grow-loop paused` status; absence of Grow Loop status means the runtime rhythm is no longer active.
152
152
 
153
- With the optional `pi-telegram` extension installed, the Telegram Status screen mirrors the same rhythm and stays hidden whenever the terminal status is hidden: `Grow Loop: #N ยท waiting`, `#N ยท 2.4s` during the countdown, and `#N ยท running` while the loop-scheduled turn is active. The mirror reads the same runtime state, fails open without the transport, and changes no scheduling behavior.
153
+ With the optional `pi-telegram` extension installed, the Telegram Status screen mirrors the same identity and stays hidden whenever the terminal status is hidden: `Grow Loop: โˆžN` for the armed or running iteration, with no countdown or phase text. The mirror reads the same runtime state, fails open without the transport, and changes no scheduling behavior.
154
154
 
155
155
  ## Interruption Model
156
156
 
@@ -71,9 +71,7 @@ export function getExistingExtensionSkillPaths(extensionUrl: string): string[] {
71
71
  }
72
72
 
73
73
  export function formatGrowLoopTelegramValue(progress: GrowLoopTelegramProgress): string {
74
- if (progress.state === "countdown") return `#${progress.iteration} ยท ${(progress.remainingSeconds ?? 0).toFixed(1)}s`;
75
- if (progress.state === "running") return `#${progress.iteration} ยท running`;
76
- return `#${progress.iteration} ยท waiting`;
74
+ return `โˆž${progress.iteration}`;
77
75
  }
78
76
 
79
77
  async function registerGrowLoopTelegramStatus(
@@ -195,12 +193,8 @@ export default function growLoopExtension(
195
193
  };
196
194
  const telegramStatusProvider = (): GrowLoopTelegramStatusLine | undefined => {
197
195
  if (pendingIteration) {
198
- if (pendingIteration.countdownStartedAt === undefined) {
199
- return { label: "Grow Loop", value: formatGrowLoopTelegramValue({ iteration, state: "waiting" }) };
200
- }
201
- const elapsed = Date.now() - pendingIteration.countdownStartedAt;
202
- const remainingSeconds = Math.max((pendingIteration.countdownDelayMs ?? 0) - elapsed, 0) / 1000;
203
- return { label: "Grow Loop", value: formatGrowLoopTelegramValue({ iteration, state: "countdown", remainingSeconds }) };
196
+ const state = pendingIteration.countdownStartedAt === undefined ? "waiting" : "countdown";
197
+ return { label: "Grow Loop", value: formatGrowLoopTelegramValue({ iteration, state }) };
204
198
  }
205
199
  if (runningIteration !== undefined) {
206
200
  return { label: "Grow Loop", value: formatGrowLoopTelegramValue({ iteration: runningIteration, state: "running" }) };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-grow-loop",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "Semantic loop-engineering for agent-owned, visible, interruptible continuation in Pi",
6
6
  "keywords": [
@@ -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.
@@ -43,7 +43,7 @@
43
43
  - `/state-flow-stop` must persist only the current session/branch's `config.enabled = false` and necessary runtime provenance, preserving all semantic checkpoints/tails and creating no semantic transition. It must retain a same-physical-session bounded passive context handoff across reload, resume, and tree restoration so disabling semantics never re-exposes pre-stop raw history. Active restart replaces passive mode but uses that bounded boundary for its one migration run; new and forked physical sessions inherit neither projection. It does not rewrite agent-level `autoStart` or change its policy for future new sessions.
44
44
  - Keep the injected runtime protocol compact and normative; put rationale and extended explanation in README rather than the model prompt. Never parse or strip State Flow HTML comments; they are ordinary historical text, while foreign comment handling remains owned by other extensions.
45
45
  - Do not claim strict boundedness for state, the current run trajectory, the turn specification, or the external full trace.
46
- - Remain extension-agnostic in core semantics, storage, and inference: core modules never import, name, special-case, or encode policy for another extension or transport. One optional leaf presentation adapter (`lib/telegram.ts`) may import public `pi-telegram` membranes to mirror status and expose the same start/stop affordances already owned by `state-flow-start` and `state-flow-stop`; it must fail open when the transport is absent or its registry is unready and must never alter core behavior.
46
+ - Remain extension-agnostic in core semantics, storage, and inference: core modules never import, name, special-case, or encode policy for another extension or transport. One optional leaf presentation adapter (`lib/telegram.ts`) may import public `pi-telegram` membranes to mirror the live status on exactly one main-menu section button and expose the same start/stop affordances already owned by `state-flow-start` and `state-flow-stop`; it must fail open when the transport is absent or its registry is unready and must never alter core behavior.
47
47
  - Activate State Flow model tools only while enabled on the selected branch; preserve every unrelated active tool when toggling them. Keep mutation confined to `patch_state` and historical observation read-only.
48
48
  - Keep `.github/workflows/release.yml` as the sole version-tag release owner: it validates immutable tag identity, publishes through npm Trusted Publisher with provenance, verifies the public package, and only then creates the GitHub Release. Keep package, lockfile, tag, and changelog versions aligned; never add a long-lived npm token fallback.
49
49
  - Run `npm run validate` after retained code changes.
@@ -1,6 +1,6 @@
1
1
  # BACKLOG
2
2
 
3
- Completed release work belongs in [CHANGELOG.md](CHANGELOG.md). No implementation item remains open for 0.8.0.
3
+ Completed release work belongs in [CHANGELOG.md](CHANGELOG.md). No implementation item remains open for 0.9.0.
4
4
 
5
5
  ## Candidate evolution
6
6
 
@@ -8,6 +8,9 @@ Completed release work belongs in [CHANGELOG.md](CHANGELOG.md). No implementatio
8
8
  - 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.
9
9
  - 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.
10
10
  - Status: candidate, not scheduled. Do not start without a dedicated release contract.
11
+ - [ ] **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.
12
+ - Boundary: presentation only. Reuse existing status diagnostics; no new semantic mode, and never mutate state from the analyzer view.
13
+ - Status: candidate, not scheduled. Do not start without a dedicated release contract.
11
14
 
12
15
  ## Deferred host integration
13
16
 
@@ -2,6 +2,20 @@
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
+
10
+ ## 0.9.2: Single Telegram surface
11
+
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.
13
+
14
+ ## 0.9.1: Status text mirrors the terminal line
15
+
16
+ - `Interop`: The `pi-telegram` surfaces now copy the compact terminal status instead of decorating it. The Status screen shows the terminal value itself (`State Flow: #N`) and hides entirely while State Flow is off; the main-menu button shows `๐ŸŒ€ State Flow: #N` or a plain `โšซ๏ธ State Flow` without on/off, starting, bootstrap, or separator text.
17
+ - `Testing`: The shared test harness removes its temporary fixture repositories at process exit; runaway fixtures previously exhausted the `/tmp` inode table and made later runs fail with ENOSPC.
18
+
5
19
  ## 0.9.0: Telegram status surface
6
20
 
7
21
  - `Interop`: Added an optional fail-open `pi-telegram` presentation adapter. With the transport installed, the status screen shows `State Flow: on ยท step #N` while enabled, and the inline main menu places a `๐ŸŒ€ State Flow: #N` or `โšซ๏ธ State Flow: off` section button directly before Settings.
@@ -66,7 +66,7 @@ Stopping changes the current session/branch configuration, preserves semantic ch
66
66
 
67
67
  The compact status is an accent `state-flow` and dim `#<step>`. Detailed status distinguishes runtime config/meta from semantic materialization, reports scope keys, the selected temporal head and runtime revision, available hot offsets, per-scope retained patch tails, artifact counts, stale Markdown reasons, and pending publication. Retained tails may include inherited pre-origin records, so their counts are not active history depth. It labels global/CWD/session/effective state without dumping source bodies. Failed inspection means unavailable evidence, never a falsely clean count.
68
68
 
69
- When the optional `pi-telegram` extension is installed, State Flow mirrors the same identity there: the Status screen shows `State Flow: on ยท step #N` while enabled, and the inline main menu places a `๐ŸŒ€ State Flow: #N` or `โšซ๏ธ State Flow: off` section button directly before Settings. The button opens a compact control submenu for the current session branch: `Start` or `Stop`, `Refresh`, and `Cancel start` while a start deferred behind an active run is pending. Starting while a run is active waits for that run to settle; stopping applies immediately. Registration fails open when the transport is absent, retries when the section registry becomes ready, and disposes on session shutdown; the `/state-flow-*` commands remain the authority and nothing changes for hosts without `pi-telegram`.
69
+ When the optional `pi-telegram` extension is installed, State Flow appears exactly once in Telegram: the inline main menu places a `๐ŸŒ€ State Flow: #N` or `โšซ๏ธ State Flow` section button directly before Settings, and that button's label carries the live status. The button opens a compact control submenu for the current session branch: `Start` or `Stop`, `Refresh`, and `Cancel start` while a start deferred behind an active run is pending. Starting while a run is active waits for that run to settle; stopping applies immediately. Registration fails open when the transport is absent, retries when the section registry becomes ready, and disposes on session shutdown; the `/state-flow-*` commands remain the authority and nothing changes for hosts without `pi-telegram`.
70
70
 
71
71
  ## State, scope, and time
72
72
 
@@ -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
 
@@ -199,7 +199,7 @@ Unknown keys fail loading. State Flow memory ownership and global availability a
199
199
 
200
200
  `/state-flow-status` reports branch mode, runtime revision, temporal head/history depth, scope keys, patch tails, artifact freshness, memory-bearing scopes, external-promotion summaries, remote policy/queue state, and pending publication. Unavailable materialization is reported as unavailable, never fabricated as empty. Artifact source bodies are not read for status.
201
201
 
202
- With `pi-telegram` installed, the optional `telegram` adapter mirrors the same identity in Telegram: the Status screen row shows `State Flow: on ยท step #N` while enabled, and the main-menu section button before Settings opens the branch start/stop submenu. Registration is fail-open and retried on session start, disposal happens on session shutdown, the adapter reads the same snapshot as the compact terminal status, and its start/stop actions call the same functions as the commands.
202
+ With `pi-telegram` installed, the optional `telegram` adapter mirrors the same identity in Telegram: the main-menu section button before Settings carries the live `๐ŸŒ€ State Flow: #N` or `โšซ๏ธ State Flow` label and opens the branch start/stop submenu. Registration is fail-open and retried on session start, disposal happens on session shutdown, the adapter reads the same snapshot as the compact terminal status, and its start/stop actions call the same functions as the commands.
203
203
 
204
204
  Opt-in `logging` appends local JSONL diagnostics for rejected `patch_state` calls and preserved drafts plus fallback turns while turn resolution is pending. A rejected call retains its exact attempted arguments, the precise error, and, when available, tool identity, call id, resolution attempt, and terminal-eligibility state; accepted patches are never logged. Records preserve useful exact text blocks and reduce other blocks to structural identity without duplicating reasoning. They are never semantic state, scope `meta.json`, Pi checkpoints, or repository input. Write failure changes no resolution, enablement, or accepted state and reports at most one bounded local warning.
205
205
 
@@ -189,7 +189,6 @@ export {
189
189
  buildStateFlowSectionView,
190
190
  createStateFlowTelegramAdapter,
191
191
  formatStateFlowSectionLabel,
192
- formatStateFlowStatusLine,
193
192
  loadStateFlowTelegramModules,
194
193
  STATE_FLOW_TELEGRAM_ID,
195
194
  type StateFlowTelegramAdapter,
@@ -202,8 +201,6 @@ export {
202
201
  type StateFlowTelegramSectionContext,
203
202
  type StateFlowTelegramSectionModule,
204
203
  type StateFlowTelegramSnapshot,
205
- type StateFlowTelegramStatusLine,
206
- type StateFlowTelegramStatusModule,
207
204
  type StateFlowTelegramView
208
205
  } from "./lib/telegram.ts";
209
206
  export { advanceTemporalState, readTemporalState, type TemporalState } from "./lib/temporal.ts";
@@ -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,13 +1,9 @@
1
- // Domain: optional pi-telegram presentation adapter for State Flow status and branch controls.
1
+ // Domain: optional pi-telegram presentation adapter for the State Flow main-menu section.
2
2
  //
3
3
  // This is a leaf adapter. Core semantics, storage, and inference never depend on it; when
4
4
  // pi-telegram is absent or its registry is not ready, registration fails open and retries.
5
5
 
6
6
  export const STATE_FLOW_TELEGRAM_ID = "@llblab/pi-state-flow";
7
- const STATUS_IMPORT_SPECIFIERS = [
8
- "@llblab/pi-telegram/status",
9
- new URL("../../pi-telegram/api/status.ts", import.meta.url).href,
10
- ];
11
7
  const SECTIONS_IMPORT_SPECIFIERS = [
12
8
  "@llblab/pi-telegram/sections",
13
9
  new URL("../../pi-telegram/api/sections.ts", import.meta.url).href,
@@ -20,11 +16,6 @@ export interface StateFlowTelegramSnapshot {
20
16
  startPending: boolean;
21
17
  }
22
18
 
23
- export interface StateFlowTelegramStatusLine {
24
- label: string;
25
- value: string;
26
- }
27
-
28
19
  export interface StateFlowTelegramButton {
29
20
  text: string;
30
21
  callback_data: string;
@@ -47,13 +38,6 @@ export interface StateFlowTelegramCallbackContext extends StateFlowTelegramSecti
47
38
  payload: string;
48
39
  }
49
40
 
50
- export interface StateFlowTelegramStatusModule {
51
- registerTelegramStatusLineProvider(
52
- provider: (ctx: { activeModel?: unknown }) => StateFlowTelegramStatusLine | undefined,
53
- options: { id: string },
54
- ): () => void;
55
- }
56
-
57
41
  export interface StateFlowTelegramSectionModule {
58
42
  registerTelegramSection(section: {
59
43
  id: string;
@@ -65,7 +49,6 @@ export interface StateFlowTelegramSectionModule {
65
49
  }
66
50
 
67
51
  export interface StateFlowTelegramModules {
68
- status?: StateFlowTelegramStatusModule;
69
52
  sections?: StateFlowTelegramSectionModule;
70
53
  }
71
54
 
@@ -90,16 +73,9 @@ export interface StateFlowTelegramAdapter {
90
73
  dispose(): void;
91
74
  }
92
75
 
93
- /** The Status screen mirrors the terminal status identity: hidden while State Flow is off. */
94
- export function formatStateFlowStatusLine(snapshot: StateFlowTelegramSnapshot): StateFlowTelegramStatusLine | undefined {
95
- if (!snapshot.enabled) return snapshot.startPending ? { label: "State Flow", value: "startingโ€ฆ" } : undefined;
96
- return { label: "State Flow", value: snapshot.bootstrap ? "bootstrap" : `on ยท step #${snapshot.step}` };
97
- }
98
-
99
- /** Main-menu section label doubles as the live status text. */
76
+ /** Main-menu section label doubles as the live status value; the disabled row carries no status text. */
100
77
  export function formatStateFlowSectionLabel(snapshot: StateFlowTelegramSnapshot): string {
101
- if (!snapshot.enabled) return snapshot.startPending ? "๐ŸŒ€ State Flow: startingโ€ฆ" : "โšซ๏ธ State Flow: off";
102
- return snapshot.bootstrap ? "๐ŸŒ€ State Flow: bootstrap" : `๐ŸŒ€ State Flow: #${snapshot.step}`;
78
+ return snapshot.enabled ? `๐ŸŒ€ State Flow: #${snapshot.step}` : "โšซ๏ธ State Flow";
103
79
  }
104
80
 
105
81
  export function buildStateFlowSectionView(
@@ -177,17 +153,12 @@ async function importTelegramModule<TModule>(
177
153
 
178
154
  /** Default loader; injectable so tests and embedded hosts can control transport presence. */
179
155
  export async function loadStateFlowTelegramModules(): Promise<StateFlowTelegramModules> {
180
- const status = await importTelegramModule<StateFlowTelegramStatusModule>(
181
- STATUS_IMPORT_SPECIFIERS,
182
- (module): module is StateFlowTelegramStatusModule =>
183
- typeof (module as StateFlowTelegramStatusModule | undefined)?.registerTelegramStatusLineProvider === "function",
184
- );
185
156
  const sections = await importTelegramModule<StateFlowTelegramSectionModule>(
186
157
  SECTIONS_IMPORT_SPECIFIERS,
187
158
  (module): module is StateFlowTelegramSectionModule =>
188
159
  typeof (module as StateFlowTelegramSectionModule | undefined)?.registerTelegramSection === "function",
189
160
  );
190
- return { ...(status === undefined ? {} : { status }), ...(sections === undefined ? {} : { sections }) };
161
+ return { ...(sections === undefined ? {} : { sections }) };
191
162
  }
192
163
 
193
164
  export function createStateFlowTelegramAdapter(options: {
@@ -196,7 +167,6 @@ export function createStateFlowTelegramAdapter(options: {
196
167
  }): StateFlowTelegramAdapter {
197
168
  const load = options.load ?? loadStateFlowTelegramModules;
198
169
  let generation = 0;
199
- let statusRegistered = false;
200
170
  let sectionRegistered = false;
201
171
  let registration: Promise<boolean> | undefined;
202
172
  const disposers: Array<() => void> = [];
@@ -211,22 +181,6 @@ export function createStateFlowTelegramAdapter(options: {
211
181
  }
212
182
  // A shutdown during loading must not leave a registration behind.
213
183
  if (epoch !== generation) return false;
214
- if (!statusRegistered && modules.status) {
215
- try {
216
- const dispose = modules.status.registerTelegramStatusLineProvider(
217
- () => formatStateFlowStatusLine(options.port.snapshot()),
218
- { id: STATE_FLOW_TELEGRAM_ID },
219
- );
220
- if (epoch === generation) {
221
- disposers.push(dispose);
222
- statusRegistered = true;
223
- } else {
224
- dispose();
225
- }
226
- } catch {
227
- // Registry not initialized yet; the next ensure retries.
228
- }
229
- }
230
184
  if (!sectionRegistered && modules.sections) {
231
185
  try {
232
186
  const dispose = modules.sections.registerTelegramSection(buildStateFlowTelegramSection(options.port));
@@ -240,12 +194,12 @@ export function createStateFlowTelegramAdapter(options: {
240
194
  // Registry not initialized yet; the next ensure retries.
241
195
  }
242
196
  }
243
- return statusRegistered || sectionRegistered;
197
+ return sectionRegistered;
244
198
  };
245
199
 
246
200
  return {
247
201
  async ensure(): Promise<boolean> {
248
- if (statusRegistered && sectionRegistered) return true;
202
+ if (sectionRegistered) return true;
249
203
  registration ??= register().finally(() => {
250
204
  registration = undefined;
251
205
  });
@@ -260,7 +214,6 @@ export function createStateFlowTelegramAdapter(options: {
260
214
  // Disposal is best-effort; pi-telegram owns its registry lifetime.
261
215
  }
262
216
  }
263
- statusRegistered = false;
264
217
  sectionRegistered = false;
265
218
  },
266
219
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.9.0",
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": [
@@ -4,6 +4,10 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.45.5: Queue Refresh Icon Hotfix
8
+
9
+ - `Queue Refresh Icon`: The queue menu's Refresh row now uses `๐Ÿ”„`, the canonical refresh glyph, reserving `๐ŸŒ€` for the State Flow Telegram identity. The button label is the only change; queue refresh behavior, callbacks, and the rotating empty-queue notices are unchanged.
10
+
7
11
  ## 0.45.4: Draft Cadence Hotfix
8
12
 
9
13
  - `Draft Cadence`: Each preview segment now holds its first frame for one full two-second interval from its first visible text, so the opening draft is an accumulated passage instead of a single streamed word. Later frames keep the trailing cadence, message/turn rollover preserves the remaining interval and reopens the window, and sealing or final publication still cancels the pending timer; first frames no longer ship immediately.
@@ -48,7 +48,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
48
48
  | `โน๏ธ` | Abort current Pi work | `/abort` command description and active `/stop` result | Stops active work; accompanying copy states separately when queued work is cleared. |
49
49
  | `๐ŸŸฅ` | Destructive stop command | `/stop` command description | Strong warning at the command/action entrypoint; standalone results use the more precise idle or abort state icon. |
50
50
  | `๐Ÿ†•` | New session / fresh start | Reserved visible extension command example for `/new`-like flows | Same-thread Telegram `/new` is currently blocked by Pi core API; keep this meaning reserved. |
51
- | `๐ŸŒ€` | Refresh | Queue refresh row and future refresh buttons | Re-fetch/re-render current surface, not transport reconnect. |
51
+ | `๐Ÿ”„` | Refresh | Queue refresh row and future refresh buttons | Re-fetch/re-render current surface, not transport reconnect. |
52
52
  | `โ†ช๏ธ` | Reroute to an existing target | Thread chooser buttons that send a captured command/message from one thread to another live thread | Curved arrow means the message arrived here but bends to another target. |
53
53
  | `๐Ÿ”` | Replace/restore mode | Thread replace/restore chooser entrypoints | Opens a second step for moving a Pi instance binding to the current source thread. |
54
54
  | `โžก๏ธ` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
@@ -88,7 +88,7 @@ function buildTelegramQueueMenuReplyMarkup(
88
88
  items.length === 0
89
89
  ? `queue:refresh:${nextEmptyRefreshIndex}`
90
90
  : "queue:refresh";
91
- const refreshRow = [{ text: "๐ŸŒ€ Refresh", callback_data: refreshData }];
91
+ const refreshRow = [{ text: "๐Ÿ”„ Refresh", callback_data: refreshData }];
92
92
  if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
93
93
  const rows = items.map((item) => {
94
94
  const prefix = item.reactionSuppressionEmoji
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.45.4",
3
+ "version": "0.45.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -43,9 +43,9 @@
43
43
  "@llblab/pi-actors": "0.53.0",
44
44
  "@llblab/pi-clean-room": "0.1.1",
45
45
  "@llblab/pi-codex-usage": "0.9.4",
46
- "@llblab/pi-grow-loop": "0.8.0",
47
- "@llblab/pi-state-flow": "0.9.0",
48
- "@llblab/pi-telegram": "0.45.4",
46
+ "@llblab/pi-grow-loop": "0.8.1",
47
+ "@llblab/pi-state-flow": "0.9.3",
48
+ "@llblab/pi-telegram": "0.45.5",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [