@llblab/pi-kit 0.20.0 → 0.21.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +6 -6
  4. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
  5. package/node_modules/@llblab/pi-state-flow/README.md +4 -4
  6. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
  7. package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -3
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +41 -11
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +6 -0
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +94 -1
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +2 -5
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +11 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +4 -2
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +8 -2
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +6 -1
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +19 -6
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +5 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +27 -3
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +2 -2
  23. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  24. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +10 -8
  25. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +2 -2
  26. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +4 -3
  27. package/node_modules/@llblab/pi-state-flow/docs/usage.md +5 -5
  28. package/node_modules/@llblab/pi-state-flow/index.ts +2 -2
  29. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -3
  30. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +41 -9
  31. package/node_modules/@llblab/pi-state-flow/lib/git.ts +83 -1
  32. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +2 -4
  33. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +11 -0
  34. package/node_modules/@llblab/pi-state-flow/lib/status.ts +10 -4
  35. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +23 -6
  36. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +29 -3
  37. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +2 -2
  38. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  39. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
  40. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
  41. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -0
  42. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -0
  43. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +3 -1
  44. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +6 -1
  45. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +5 -0
  46. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +9 -0
  47. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +5 -1
  48. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +8 -0
  49. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +63 -11
  50. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +4 -0
  51. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +14 -0
  52. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  53. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +5 -0
  54. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  55. package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
  56. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  57. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  58. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +7 -0
  59. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -2
  60. package/node_modules/@llblab/pi-telegram/lib/commands.ts +19 -0
  61. package/node_modules/@llblab/pi-telegram/lib/extension.ts +9 -0
  62. package/node_modules/@llblab/pi-telegram/lib/queue.ts +75 -16
  63. package/node_modules/@llblab/pi-telegram/lib/replies.ts +18 -0
  64. package/node_modules/@llblab/pi-telegram/lib/routing.ts +7 -0
  65. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  66. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.21.1 - 2026-09-23
6
+
7
+ - `Queue Transition Reliability`: Advances the exact Telegram pin to `0.51.1`. Busy `/next` now reports the interrupted turn and exact selected queued prompt in order, preserves one reply-header owner through agent start, keeps rapid ordinary messages separate, and lets `/abort` or `/stop` cancel stale notices without allowing notice failures to block dispatch.
8
+ - `Follower Heartbeat Stability`: Uses a dedicated eight-second follower heartbeat response deadline aligned with leader stale-liveness policy, preventing ordinary multi-second Pi/TUI event-loop stalls from destroying a healthy socket, producing leader `EPIPE` noise, or cycling registration status.
9
+ - `Package Cohort`: Keeps every other bundled package at its current exact version. Package membership, resource paths, load order, Pi minimum, and bundled Skill ownership remain unchanged.
10
+
11
+ ## 0.21.0 - 2026-09-23
12
+
13
+ - `Independent Scope Revisions`: Advances the exact State Flow pin to `0.18.0`. Global, CWD, and Session now persist independent semantic revision counters, Effective displays the truthful `G#/C#/S#` vector, and Session exclusively owns response state while Global and CWD retain only their structural placeholders.
14
+ - `Progressive Backup Replication`: Settled-turn backup attempts asynchronously push the exact current commit to the attached branch's explicitly configured remote/ref without force. Canonical acceptance and response delivery never wait for or roll back on push failure, and a later accepted turn retries the latest backup.
15
+ - `Lifecycle And Status`: Empty ordinary completions now finalize normally and preserve preceding state transitions. Active terminal status uses `state-flow G#/C#/S#`, while Telegram keeps `State Flow: G#/C#/S#`; passive terminal status remains absent and Telegram reports State Flow as off.
16
+ - `Package Cohort`: Keeps every other bundled package at its current exact version. Package membership, resource paths, load order, Pi minimum, and the single bundled `show-me` ownership path remain unchanged.
17
+
5
18
  ## 0.20.0 - 2026-09-23
6
19
 
7
20
  - `Telegram Workspace Safety`: Advances the exact Telegram pin to `0.51.0`, adding demand-only A–Z slot rotation with dead-owner custody proof, protected inactivity preservation, durable rejection recovery, canonical profile storage paths, and settled leader-election display reconciliation.
package/README.md CHANGED
@@ -14,15 +14,15 @@ 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.10.0` | Compact Codex/Spark subscription-limit and Business credit-usage 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.17.4` | Incremental scoped context/memory compiler with canonical file persistence, optional Git backups, native working context within each run, and targeted historical reads |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.0` | Telegram companion with pressure-safe Workspace slot rotation, fenced follower readiness, adaptive Thread continuity, exact queues, files, voice, controls, and bundled Telegram interaction Skills |
17
+ | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.18.0` | Incremental scoped context/memory compiler with independent scope revisions, canonical file persistence, optional replicated Git backups, native working context within each run, and targeted historical reads |
18
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.1` | Telegram companion with stable follower heartbeats, exact queue-transition notices, pressure-safe Workspace rotation, adaptive Thread continuity, files, voice, controls, and bundled Telegram interaction Skills |
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.
22
22
 
23
23
  ## Install
24
24
 
25
- Requires **Pi 0.87.0+** and **Node.js 22.19.0+**. State Flow 0.17 introduces a breaking storage-format boundary with no in-place predecessor converter. Preserve existing State Flow stores and review the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.17.4/docs/usage.md#moving-a-store-and-the-017-format-boundary) before upgrading from an earlier kit.
25
+ Requires **Pi 0.87.0+** and **Node.js 22.19.0+**. State Flow 0.17 introduces a breaking storage-format boundary with no in-place predecessor converter. Preserve existing State Flow stores and review the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.18.0/docs/usage.md#moving-a-store-and-the-017-format-boundary) before upgrading from an earlier kit.
26
26
 
27
27
  From npm:
28
28
 
@@ -3,7 +3,7 @@
3
3
  - Keep independent domain modules under `lib/`, mirror every domain with a same-named file under `tests/`, place cross-domain architecture checks in `tests/invariants.test.ts`, and keep `index.ts` as a minimal public-export boundary. Keep `lib/extension.ts` as the Pi lifecycle composition root: it may wire configuration, domain capabilities, handlers, and event subscriptions, but low-level parsing, traversal, formatting, persistence, queue/worker mechanics, and diagnostic I/O belong to their existing owning domains.
4
4
  - Keep the extension opt-in and preserve clear attribution to SKILL.state wherever the inherited explicit-state approach is described.
5
5
  - Preserve Pi's native tool loop and complete inspectable session trace; project completed-run history only at user-run boundaries. The runtime-context builder owns model projection of the raw scope overlay; callers must not pre-project it. Retain persistent and current-run context-bearing custom messages from other extensions, plus the complete current-run trajectory. Select a unique captured native user timestamp independently of text decoration, including SDK image hints. Observe native user events even while disabled; Start/Stop must retain that observed run anchor. Reset it at native before-agent-start and session-start/tree boundaries, not semantic mode changes. Observation alone never enables state, publishes memory or authorizes compaction. Without a capture, a unique exact specification match may select only model projection. Missing, nonfinite or ambiguous matches retain available context; projection must never assign the lifecycle run anchor. Direct completion persists no private validation feedback.
6
- - Expose one canonical intent-first materialized state shape across global, CWD, and session scopes with exactly `intents`, `contract`, `working`, `artifacts`, `response`, and `lazy`; the first four and the required `lazy` root are flexible semantic objects, while `response` is the latest complete user-facing answer string where applicable. Only model baseline projection omits lazy bodies. Preserve this plane order in model-facing projection and state presentation. `intents` contains only active commitments to future action, remains hot by default, and must not become a planner, scheduler, task manager, or execution loop.
6
+ - Expose one canonical intent-first materialized state shape across global, CWD, and session scopes with exactly `intents`, `contract`, `working`, `artifacts`, `response`, and `lazy`; the first four and the required `lazy` root are flexible semantic objects, while `response` is the exact latest accepted assistant answer, including the empty string, and is owned only by session state. Global and CWD keep the required key as an empty structural placeholder; effective state inherits the session value. Only model baseline projection omits lazy bodies. Preserve this plane order in model-facing projection and state presentation. `intents` contains only active commitments to future action, remains hot by default, and must not become a planner, scheduler, task manager, or execution loop.
7
7
  - Key artifacts directly by their source path. Model-visible artifact entries require only a non-empty `description` plus optional forward-compatible metadata; runtime-owned compilation evidence (`sourceFingerprint`, `sourceHash`, `compilerRevision`, `compiledAt`) belongs in the scope `meta.json` provenance registry. Reserve model-authored `hint`; runtime may add it only to effective/model projection and never to canonical scope state. Retired embedded `hash`/`compiler`/`compiled_at` fields remain readable compatibility evidence and are stripped from model projection. Validate provenance restrictions on every authored artifact entry, not on retained legacy state: individual runtime/legacy provenance fields cannot be set or deleted by model patches in any scope.
8
8
  - Inspect only exact artifact source paths already registered in global, CWD, or session state. Never scan directories, discover files, reserve a Knowledge root, interpret file extensions, or read unrelated source bodies. Observe regular non-symlink files through `size + mtimeNs`; proven absence removes the artifact and provenance from each exact owning scope, while relative, symlink, directory, inaccessible, or otherwise unavailable paths are non-destructive evidence.
9
9
  - Use `classifyArtifactCompilationNeed` for public acquisition/rehydration and ordinary-artifact Pi invalidations. Compare observed fingerprints with hidden provenance; missing/malformed retained fingerprints, malformed compiler evidence, and compiler changes request compilation without declaring semantic corruption. Fingerprint-only decisions ignore unused legacy hashes, while explicit current hashes retain their checks; never invent hashes in read plans. Accept signed filesystem `mtimeNs`, including pre-epoch sources. Preserve changed-source semantics and add `hint` only to model projection. Effective Skill entries mask lower ordinary entries and retain their separate hash protocol.
@@ -21,16 +21,16 @@
21
21
  - 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. 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 persistence/process boundaries. Serialize cooperating canonical-file writers through file-cohort exclusion and CAS. 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. Inspect only exact already-registered artifact paths; do not discover or own a Knowledge directory.
22
22
  - At instance initialization resolve the repository, CWD key, and session key; load each scope's anchored canonical checkpoint and tail, materialize at the selected retained boundary, then overlay `global → cwd → session`. Install cached view and file publication basis atomically only after successful restoration/initialization. Revalidate mutable file cohorts; never turn inspection into a long-lived publication-basis cache. Preserve selected retained-boundary evidence across transient restore failure so 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 follows the [session-copy contract](docs/fork-contract.md): copy the proven retained source-session stream and provenance into a distinct fresh canonical origin over current shared streams, without modifying parent-private files. Reject occupied targets and checkpoint the child only after CAS acceptance. Inherited parent pointers never authorize an empty reset; fence parent passive Stop projection across child reload.
23
23
  - Bind active Pi checkpoints to retained semantic boundaries in the current canonical lineage. Resume and tree restoration select only retained private session history while global/CWD scopes remain live; every failure resolving a selected boundary fails closed without falling through to older checkpoints, disabled markers, Git revisions, or unrelated newer private state. Passive shared-memory availability does not prove the selected private layer: preserve the recovery failure, reject session reads and all publication until it is resolved, and let explicit Start retry the exact selection. Never checkpoint a passive substitute over failed branch recovery.
24
- - Every accepted semantic change, including session-only and response-only changes, atomically updates affected canonical checkpoint/tail pairs and temporal metadata without creating a Git commit. After an accepted turn reconciles its response and reaches `agent_before_settle`, one best-effort backup may commit only State Flow-owned already-written files. Acquire the backup mutex before briefly locking canonical storage for bounded root/CWD/session namespace inventory and regular-file byte capture; canonical writers never acquire the backup mutex. Release the canonical lock before every Git command, filter, staging write, ref update, or index synchronization. Stage only the captured snapshot through a private temporary worktree/index, preserve Git ignore/filter policy, and never inventory artifact sources or unrelated directory trees. Keep unrelated staged/index-only content and worktree edits intact; synchronize only exact owned index paths, include HEAD-owned deletions even when already absent from the caller index, and never create an unowned-only initial backup. Backup failure is diagnostic-only and never rolls back state, suppresses an answer, requests continuation, or blocks a later turn. Durable push queues, publication workers, worker leases, and remote retry generations do not exist.
24
+ - Every accepted semantic change, including session-only and response-only changes, atomically updates affected canonical checkpoint/tail pairs and temporal metadata without creating a Git commit. Each materially changed scope advances its own persisted revision exactly once; Global and CWD revisions are shared across their writers, Session is private, and Effective is represented by the `G#/C#/S#` revision vector rather than an invented scalar owner. After an accepted turn reconciles its response and reaches `agent_before_settle`, one best-effort backup may commit only State Flow-owned already-written files. Acquire the backup mutex before briefly locking canonical storage for bounded root/CWD/session namespace inventory and regular-file byte capture; canonical writers never acquire the backup mutex. Release the canonical lock before every Git command, filter, staging write, ref update, or index synchronization. Stage only the captured snapshot through a private temporary worktree/index, preserve Git ignore/filter policy, and never inventory artifact sources or unrelated directory trees. Keep unrelated staged/index-only content and worktree edits intact; synchronize only exact owned index paths, include HEAD-owned deletions even when already absent from the caller index, and never create an unowned-only initial backup. Backup failure is diagnostic-only and never rolls back state, suppresses an answer, requests continuation, or blocks a later turn. After each successful backup attempt, asynchronously push the exact current commit to the attached branch's explicitly configured remote/ref without force. Push failure is visible but has no semantic effect; a later accepted turn retries the latest backup. Durable push queues, publication workers, worker leases, and remote retry generations do not exist.
25
25
  - Register `patch_state` as the sole model-authored semantic mutation protocol. Its canonical grammar accepts one or more fixed `global`, `cwd`, and `session` semantic patches; reject unknown fields, empty supplied scopes, material no-ops, and retired finalization or `{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`.
26
26
  - Treat every semantic `patch_state` as a strict inference barrier. Give it sequential execution mode; find the matching synchronized assistant through public parent traversal from the selected leaf without constructing a full branch during tool preflight; require exactly one `patch_state` call in that response and block every sibling tool call before execution. The next inference sees rematerialized state. Ordinary accepted answers require no finalization patch or fallback inference.
27
- - 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. Other extensions retain ownership of their own comments and output handling.
27
+ - Reconcile `response` only from the actually accepted ordinary assistant answer at `turn_end`; it remains runtime-owned, and an accepted empty answer reconciles to `""` rather than causing a finalization failure. State Flow has no terminal HTML-comment mutation protocol and does not parse generic service comments. Other extensions retain ownership of their own comments and output handling.
28
28
  - Treat terminal state as a decision-relevant handoff, not narration: retain source-addressed reusable operational knowledge in `artifacts`; compile stable requirements, confirmed decisions, rejected approaches, and interface commitments into `contract`; retain observations, validation, failures, current domain state, unresolved work, interaction consequences, and exact continuation in `working`. Preserve relevant completed prerequisites and verified outcomes while removing obsolete progress narration; reconcile only information affected by the run and relevant existing commitments, not every scope or repository surface.
29
29
  - Preserve active constraints, unresolved questions, consequential negative results, and the next discriminating check before compression. Distinguish observations, user requirements, assistant decisions, and hypotheses; do not promote assistant conclusions to user requirements. Treat resource, document, memory, Skill, and agent locators as semantic references wherever context expresses them. Inside ordinary strings and prose, encode a semantic-state path as `$` immediately followed by one valid `read_state` path, for example `$effective.lazy.memory[7]`; the prefix distinguishes a reference from incidental path-like text and leaves room for future parsing. `{"$ref":"cwd.lazy.plan"}` remains the optional structured state-reference form; neither form is a runtime link type. External resource locators retain their native path, URI, Skill, or agent syntax. Resolve references explicitly through the appropriate read/tool when needed and infer no authority, existence, dependency, hydration, execution, or completion merely from their presence. Never scan, audit, or proactively resolve references merely to find broken ones. Only after one requested `read_state` value path is missing may the query domain perform one bounded reverse lookup over current model-patchable semantic planes for exact structured `$ref` and `$path` matches. When durable sources match, return a successful top-level `{value:null, hint:[...]}` diagnostic sentinel: every hint has `type:"dangling-reference"`, an action message, and at most three runtime-verified current owning paths. The null sentinel is never presented without `hint` for this case; keys, patch, and multi-path reads retain ordinary all-or-error semantics. Never scan runtime-owned `response`, and retain the ordinary missing-path error when no durable source matches. A match proves durable semantic provenance, not staleness; no match does not prove invention. The agent may then inspect ownership and patch a proven stale source while preserving surrounding meaning. Effective-state absence does not identify ownership, and unavailable history, inaccessible external resources, or transient read failure do not prove a dangling reference. Retain useful source locators and validity conditions for consequential facts without mandatory per-value metadata. Keep rejection reasons and reconsideration conditions. Reconcile contradictions through evidence or user clarification instead of silently overwriting established constraints or observations; retain unresolved conflicts and decision-relevant hypotheses as uncertain. These are protocol obligations, not deterministic semantic validation gates.
30
30
  - Treat `working` as last observations, not a live workspace. Revalidate volatile facts before consequential actions; after interruption or branch navigation inspect relevant external effects before repeating operations. Failed state commits and restored memory do not undo tool effects. Missing evidence proves neither success nor absence of effects: retain uncertainty and the next check. Keep revalidation targeted, without action ledgers or runtime freshness/rollback guarantees.
31
31
  - Recognize Skill acquisition only when a successful finalized `read` path exactly matches a registered Pi Skill exposed by the public `getCommands()` resource metadata. Map `sourceInfo.scope` deterministically as user → global, project → CWD, and temporary → session; never infer ownership from path shape. Hash the executed source bytes. Matching current provenance creates no acquisition request. Otherwise append a concise tool-result hint naming the exact target, but keep the read volatile unless the model supplies durable compiler output; pending acquisition never blocks an unrelated semantic patch or ordinary completion. Validate an attempted compilation at the reported scope/path with non-empty `description`, `kind: "skill"`, and flexible non-empty `compilation`, then record runtime-owned `sourceHash` plus `skill-artifact-v1` `compilerRevision` in that scope's provenance. Replace the complete prior Skill artifact and provenance on refresh so obsolete evidence cannot survive. Unregistered `SKILL.md` reads have no acquisition semantics. `contract.compiled_skills` is retired and rejected; never fabricate hashes or bypass explicit source acquisition.
32
32
  - Make state stewardship an ordinary model responsibility without a background loop or gratuitous full-state rewrite. Every handoff curates touched and obviously stale or mis-scoped visible branches: place new knowledge at the narrowest valid scope, merge fragmented facts, compress history into conclusions, and delete stale, completed, redundant, or low-value keys while preserving active commitments and evidence. Dedicated cleanup and scope audits require an explicit user request; a feature/release/project boundary may motivate a recommendation, not an automatic audit. For a proven move within one store, inspect both owners, resolve conflicts, apply destination/source changes through one atomic multi-scope patch, and verify both owners plus the effective overlay. External transfers require destination-native acceptance verification before source deletion. Global is only established cross-project/user/environment knowledge, CWD is reusable project truth, and session is branch/run continuation.
33
- - Accept omitted semantic fields inside each supplied scope patch, but reject empty supplied scopes and require at least one material semantic or provenance change. When no state change is needed, do not call `patch_state`. Always require an accepted non-empty answer, which runtime owns as session `response`. A changed response is a semantic transition; identical complete semantic state finalizes lifecycle without a patch, identity, or temporal step. Semantic usefulness and optimization remain protocol-owned because deterministic validation cannot prove them.
33
+ - Accept omitted semantic fields inside each supplied scope patch, but reject empty supplied scopes and require at least one material semantic or provenance change. When no state change is needed, do not call `patch_state`. Accept the exact ordinary answer, including an empty string, which runtime owns as session `response`. A changed response is a semantic transition; identical complete semantic state finalizes lifecycle without a patch, identity, or temporal step. Semantic usefulness and optimization remain protocol-owned because deterministic validation cannot prove them.
34
34
  - Agent-level opt-in `logging` appends local JSONL diagnostics for rejected `patch_state` attempts and response-finalization failures. Every rejected call retains its exact attempted arguments plus the precise error and, when available, tool identity and call id; successful patches and accepted answers are never logged. Preserve exact text blocks only where useful, reduce other blocks to structural identity, never duplicate reasoning bodies, and keep logging outside semantic state, scope metadata, checkpoints, and publication. Logging failure emits at most one warning and never changes enablement or accepted state.
35
35
  - Keep `applyPatch` results and staged mutable drafts detached from caller patches and accepted scopes. Detach once at the public patch boundary; private owned-draft COW may share untouched paths only within that isolated basis. Clone incoming replacements, detach inherited objects before preserving their merge behavior, and retain late-response/artifact mutation isolation. Do not trade CAS, publication rollback or public/historical read isolation for cross-boundary sharing.
36
36
  - Recursively materialize patches immediately; empty objects preserve, nested object-key `null` deletes, and `null` anywhere in semantic state including arrays is invalid. This prohibition does not apply to runtime envelopes such as an origin's null parent. Persist runtime-normalized replay patches that exactly reproduce accepted state, including complete artifact replacement; runtime-owned provenance is stored in scope `meta.json` and is not part of semantic replay.
@@ -38,13 +38,13 @@
38
38
  - Rotate the turn-stable specification on every user-initiated run. Persist the full specification only while that run is unfinished, then remove it atomically with accepted terminal response reconciliation. Enabled provider requests still receive current memory when Pi's actionable turn/settle boundaries continue without another `before_agent_start`; omit an absent specification rather than resurrecting the completed prompt, inventing a user run, or taking over Pi's continuation scheduler. Keep user-controlled specification text at user authority: never interpolate it into the system prompt; repeat it only in synthetic user runtime context. Treat materialized state in that message as fallible assistant-produced data whose transport role does not elevate it into user instructions.
39
39
  - When enabled inside an existing session, retain Pi's active context for exactly one complete bootstrap run and require its `patch_state` resolution to compile all future-relevant context.
40
40
  - Restore extension state from the active Pi branch's retained-boundary checkpoint and current canonical lineage, not the full entry list, a storage receipt, or Git `HEAD`, on startup and successful tree navigation.
41
- - Render compact status as accent `state-flow` plus dim `#<step>` in both active and passive modes; every accepted passive patch advances the same counter. `/state-flow-status` must remain observational: show CWD/session keys, step, active temporal head, available hot-history depth, per-scope artifact/tail counts, and already-known invalidations; render only the effective global → CWD → session materialization as semantic JSON; perform no source reads, scans, fingerprinting, or maintenance. When temporal materialization is unavailable, report unknown counts and unavailable state rather than inventing empty projections.
41
+ - Render compact terminal status as accent `state-flow` plus dim `G#/C#/S#` only while active mode is enabled; passive changes still advance the affected scope revisions without exposing an active-status signal. `/state-flow-status` must remain observational: show CWD/session keys, internal step, independent scope revisions, active temporal head, available hot-history depth, per-scope artifact/tail counts, and already-known invalidations; render only the effective global → CWD → session materialization as semantic JSON; perform no source reads, scans, fingerprinting, or maintenance. When temporal materialization is unavailable, report unknown counts and unavailable state rather than inventing empty projections.
42
42
  - Explicit `/state-flow-start` creates the canonical file store when missing and returns after local runtime acceptance without initializing or consulting Git. It initializes missing global, CWD, and current-session checkpoint/tail pairs plus session config/runtime metadata through compare-and-swap publication, enables only the current branch, and bootstraps prior conversation when needed. Explicit start on a proven pre-runtime branch establishes an empty session origin rather than importing a later same-session layer; shared streams remain live. Ordinary new sessions remain manual unless repository-global `autoStart` is true; configured new sessions may initialize missing CWD state and receive distinct empty session layers while inheriting global/CWD values. Resumed and tree-selected branches restore their retained canonical lineage regardless of that flag.
43
43
  - `/state-flow-stop` returns to the configured passive bootstrap/tool combination. For an accepted runtime, persist only the current session/branch's `config.enabled = false` and necessary runtime provenance, preserving all semantic checkpoints/tails and creating no semantic transition. A proven pre-runtime branch instead appends only the existing `{disabled:true}` Pi checkpoint; a cached passive view never authorizes runtime publication or a retained-boundary checkpoint. Only accepted canonical publication ends that pre-runtime condition. Preserve failed-selection fences, passive access, and later explicit Start/passive patch behavior. Lifecycle-only writes adopt unrelated live shared drift at a fresh origin when needed, without touching semantic/provenance files or counters; same-session races and explicit stale provenance writes still fail closed. Do not opportunistically fold tails or prune evidence during lifecycle persistence, even if another writer loaded a larger history limit. Refresh the host cache after adoption, freeze Stop's handoff from that accepted cache, and inspect newly adopted registered paths before the next enabled inference. Retain a frozen handoff plus the active user-run trajectory (including later tool results), post-stop conversation, and foreign context-bearing custom messages across same-physical-session reload/resume/tree. Exclude completed pre-run conversation only when the active boundary is proven. If a recorded active anchor is missing, nonfinite or ambiguous after native projection/compaction, reuse the active context selector and retain available summaries and tool trajectory; never fall back to the idle cutoff or reconstruct discarded raw entries. Idle Stop still retains only later conversation and foreign custom context. Active restart replaces passive mode but uses that bounded boundary for its one bootstrap 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
  - After an accepted non-bootstrap run settles with no pending input, State Flow may request native manual compaction under a generation-private marker only when public `getContextUsage()` reports at least 24,000 tokens. Keep the complete latest accepted run from its already captured first-user anchor, including steering and tool results; never substitute the last user message. Require one matching native user timestamp before the final assistant, otherwise skip rather than guessing. Store only retained boundary/step identity in details, and never duplicate state in the summary. Unknown or smaller usage skips compaction; a benign native refusal releases the attempt for later work. Await the native compaction completion/error callback inside the settled handler before returning, so Pi's deferred companion prompt dispatch cannot race an in-flight manual compaction; do not add a timer, queue or continuation owner. Skip prefixes containing foreign custom metadata or native `custom_message` context. Never customize user manual or native threshold/overflow compaction, discard unfinished pre-patch work, create a State Flow origin, or rewrite Pi's append-only JSONL/tree.
45
45
  - Keep the injected runtime protocol compact and normative; put rationale and extended explanation in README rather than the model prompt. Contribute initial active/passive protocol through Pi's native `systemPromptOptions.sections.state_flow`, not a returned forced `systemPrompt`; refresh only that owned section at `context_with_system` from current mode so in-flight Stop/Start and boundary continuations do not retain stale instructions. Keep section projection pure in the context domain, preserve unchanged arrays/native deltas and non-system identities, and never invent missing system frames. Preserve companion sections, tool declarations, native trace and explicit foreign forced-prompt precedence. Foreign comment handling remains owned by other extensions.
46
46
  - Do not claim strict boundedness for state, the current run trajectory, the turn specification, or the external full trace.
47
- - 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 patch counter on exactly one main-menu section button, expose the same start/stop affordances already owned by `state-flow-start` and `state-flow-stop`, and read global/CWD/session/effective snapshots in active or passive mode. Telegram observation may lazily load existing shared canonical state even when passive model tools are disabled, but never initializes or mutates it; it must fail open when the transport is absent or its registry is unready and must never alter core behavior.
47
+ - 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 show the live `G#/C#/S#` effective revision vector on exactly one main-menu section button only while active mode is enabled, expose the same start/stop affordances already owned by `state-flow-start` and `state-flow-stop`, and read global/CWD/session/effective snapshots in active or passive mode. Passive main-menu identity renders `State Flow: off`; requested owner-scope Rich snapshots show `#revision`, while Effective shows the vector. Inspection may refresh live shared streams in memory but never publishes or advances a revision. Telegram observation may lazily load existing shared canonical state even when passive model tools are disabled, but never initializes or mutates it; it must fail open when the transport is absent or its registry is unready and must never alter core behavior.
48
48
  - Activate State Flow model tools while an episode is enabled or passive tools are configured; preserve every unrelated active tool when toggling them. Passive reads never initialize storage, while an explicit passive patch may initialize absent canonical storage without enabling an episode, continuation, or compaction. Unsupported predecessor storage is never converted. Keep mutation confined to `patch_state` and historical observation read-only.
49
49
  - 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.
50
50
  - Keep opt-in performance executables and workers in top-level `benchmarks/`; their correctness regressions stay in `tests/`. Benchmark workloads may reuse synthetic test fixtures, remain excluded from runtime packaging, and retain source-bound measurement identities across relocations.
@@ -2,6 +2,15 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.18.0: Independent Revisions and Replicated Backups
6
+
7
+ - `Independent scope revisions`: Persists independent Global, CWD and Session semantic counters. Each materially changed scope advances once per atomic cohort; no-ops advance none, Session owns response changes, folding preserves counts, and pre-revision 0.17 stores start from retained-tail evidence. Effective uses the truthful `G#/C#/S#` vector rather than a scalar observer count. Metadata v2 keeps v1 readable and fences older writers after a revision-aware scope write.
8
+ - `Passive status semantics`: Active terminal status renders `state-flow G#/C#/S#`, while Telegram retains its `State Flow: G#/C#/S#` label format; passive terminal status is absent and Telegram shows `State Flow: off`. Owner Rich snapshots show `#revision`, Effective shows the vector, and read-only inspection may refresh foreign Global/CWD revisions in memory without publication.
9
+ - `Response ownership`: Documents response as runtime-owned Session state. Global/CWD retain only the empty structural slot required by the uniform canonical shape and omit it from Telegram Rich views; Effective inherits the Session response.
10
+ - `Revision status freshness`: Refreshes terminal status immediately after automatic missing-artifact reconciliation, so an independently advanced Global/CWD revision is visible before any later `patch_state` call rather than appearing to have been caused by that call.
11
+ - `Progressive backup replication`: After each successful settled-turn backup attempt, asynchronously pushes the exact current commit without force to the attached branch's explicitly configured remote/ref. Push failure remains visible but cannot affect canonical acceptance or the answer; a later accepted turn retries the latest backup. No durable queue, worker, lease, retry generation, remote policy metadata or semantic Git authority returns.
12
+ - `Empty response finalization`: Accepts an ordinary completion with no text as a valid lifecycle boundary, stores the Session response as `""`, and preserves any preceding `patch_state` transition instead of emitting a blocking recovery error.
13
+
5
14
  ## 0.17.4: scoped Skill acquisition and passive observability hotfix
6
15
 
7
16
  - `Passive observability`: Keeps the `state-flow #N` counter visible in terminal and Telegram surfaces while active mode is off, with accepted passive patches advancing the same counter. Telegram scope controls now explicitly retain global/CWD/session/effective inspection in either mode and can lazily read existing shared canonical state when passive model tools are disabled, without initializing or mutating storage.
@@ -56,7 +56,7 @@ Starting in an existing conversation retains its context for one complete bootst
56
56
  - `/state-flow-status`: Inspect effective state, retained history and known recovery issues without scanning sources or changing state.
57
57
  - `/state-flow-stop`: End active episode semantics without deleting memory; an interrupted run retains its frozen handoff and available trajectory.
58
58
 
59
- Active mode is **opt-in**. Passive memory projection and the memory tools are enabled by default: existing state can be read or explicitly patched without an active episode or State Flow compaction. Passive patches advance the same `#N` transition counter. When `pi-telegram` is present, global, CWD, session and effective state remain inspectable from its State Flow section in either mode. Stop returns to the configured passive behavior. `autoStart` can enable active mode for genuinely new sessions; resumed branches restore their own enablement. See [configuration](docs/usage.md#configuration).
59
+ Active mode is **opt-in**. Passive memory projection and the memory tools are enabled by default: existing state can be read or explicitly patched without an active episode or State Flow compaction. Every materially changed owner advances its independent scope revision even in passive mode, without displaying an active-mode status. When active, terminal and Telegram status render the Effective revision vector as `G15/C8/S31`; passive Telegram shows `State Flow: off`. Requested Global, CWD and Session snapshots show their own `#revision`, while Effective shows the vector. Inspection can refresh live shared revisions from other instances without publishing state or advancing a counter. Stop returns to the configured passive behavior. `autoStart` can enable active mode for genuinely new sessions; resumed branches restore their own enablement. See [configuration](docs/usage.md#configuration).
60
60
 
61
61
  ## State model
62
62
 
@@ -80,10 +80,10 @@ Every scope uses the same shape:
80
80
  - `contract`: Requirements, decisions, constraints and interface commitments.
81
81
  - `working`: Observations, results, uncertainties and current continuation.
82
82
  - `artifacts`: Source-addressed descriptions and compiled knowledge.
83
- - `response`: The latest complete answer, captured by the runtime.
83
+ - `response`: The exact latest accepted answer, including an empty string, captured by the runtime only in Session. Global and CWD retain an empty structural slot; Effective inherits the Session value.
84
84
  - `lazy`: Supporting memory available through explicit reads, with its body omitted from baseline model context.
85
85
 
86
- These planes organize ordinary JSON rather than imposing a project-specific schema. The model updates the semantic planes except `response`, which is runtime-owned. Memory remains fallible: storing an observation does not make it current or correct.
86
+ These planes organize ordinary JSON rather than imposing a project-specific schema. The model updates the semantic planes except `response`, which is runtime-owned. Global and CWD revisions are shared by their canonical stores, Session has its own revision, and Effective has no scalar owner: its identity is the `G#/C#/S#` vector. One atomic patch advances each materially changed scope once. Memory remains fallible: storing an observation does not make it current or correct.
87
87
 
88
88
  Registered Pi Skills may be compiled into source-addressed artifacts when durable guidance is useful. Pi's resource provenance determines ownership: user Skills map to global, project Skills to CWD and temporary Skills to session. A matching source hash needs no update; an uncompiled read remains ordinary volatile context and does not block unrelated patches.
89
89
 
@@ -124,7 +124,7 @@ Array ranges, structural `keys` reads and path-intersected `patch` projections s
124
124
 
125
125
  The default store is `~/.pi/agent/state-flow/`, independent of registered source files. Canonical `checkpoint.json`, `patches.jsonl` and `meta.json` files hold each scope's state, retained changes and metadata; session configuration and runtime identity are stored separately.
126
126
 
127
- **Git backups are optional.** When the store is a configured Git repository, accepted active turns may create versioned backups of State Flow-owned files. Backup needs a Git commit identity; accepting and persisting state does not. A backup failure produces a warning without rejecting or rolling back accepted memory. Git history can be inspected separately, but it is not the authority for `read_state` or automatic restoration of expired semantic boundaries.
127
+ **Git backups are optional.** When the store is a configured Git repository, accepted active turns may create versioned backups of State Flow-owned files. If the attached branch has an explicitly configured remote, State Flow then pushes the exact current backup commit there asynchronously and without force. Commit or push failure produces a warning without rejecting or rolling back accepted memory; a later accepted turn tries the latest backup again. Backup needs a Git commit identity, but accepting and persisting state does not. Git history can be inspected separately, but it is not the authority for `read_state` or automatic restoration of expired semantic boundaries.
128
128
 
129
129
  Resume and tree navigation restore the selected retained session boundary over current shared global/CWD memory. A new session gets its own session layer. Supported native forks copy selected session state into a new owner without changing the parent's private data. Expired, incomplete or contradictory boundaries fail closed rather than silently substituting newer state. See [fork support](docs/usage.md#fork-support-and-limits) and [storage recovery](docs/usage.md#storage-and-recovery).
130
130
 
@@ -3,7 +3,7 @@ export { classifyArtifactCompilationNeed, compileArtifact, hashArtifactSource, i
3
3
  export { buildContinuationCandidates, discoverNativeSessionHeaders, inspectStateFlowContinuationProvenance, readNativeSessionHeader, recommendContinuationFromProvenance, resolveContinuationStartup, type ContinuationCandidateProvenance, type ContinuationCandidateSummary, type ContinuationHostContext, type ContinuationHostIntent, type ContinuationProjectIdentity, type ContinuationRecommendation, type ContinuationRecommender, type ContinuationSessionCandidate, type ContinuationStartupDecision, type ContinuationTransport, type NativeSessionHeader } from "./lib/continuation.ts";
4
4
  export { captureTemporalFileBases, cwdScopeKey, getDurableRepositoryRoot, isStateFlowOwnedPath, loadScopeStream, parseScopeProvenance, parseScopeStream, serializeScopeProvenance, serializeScopeStream, sessionRuntimePaths, sessionScopeKey, sessionStorageKey, temporalScopePaths, temporalStateFileUpdates, type ScopeStreamSources, type TemporalScopePaths } from "./lib/durable.ts";
5
5
  export { default, PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME, type StateFlowExtensionOptions } from "./lib/extension.ts";
6
- export { backupCurrentStateFlowFiles } from "./lib/git.ts";
6
+ export { backupCurrentStateFlowFiles, pushCurrentStateFlowBackup } from "./lib/git.ts";
7
7
  export { createAcceptedTransition, DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, projectRecentTransitionsWithLimit, validateRecentTransition, type AcceptedTransition, type RecentScopePatch, type RecentTransition, type RecentTransitionWindow } from "./lib/history.ts";
8
8
  export { applyPatch, canonicalJson, hashJson, isObject, validatePatch } from "./lib/json.ts";
9
9
  export type { JsonObject, JsonValue } from "./lib/json.ts";
@@ -14,5 +14,5 @@ export { TemporalRuntime } from "./lib/runtime.ts";
14
14
  export { hasCompiledSkillArtifact, hashSkillSource, SKILL_ARTIFACT_COMPILER, type SuccessfulSkillRead } from "./lib/skills.ts";
15
15
  export { emptyState, isMaterializedState, isStateDocument, overlayStates, projectStateForModel, updateMaterializedArtifacts, type AtomicScopePatches, type MaterializedState, type ScopedPatch, type ScopedStates, type ScopePatch, type SemanticTransition, type StateDocument, type StatePatch, type StateScope, type TerminalTransition } from "./lib/state.ts";
16
16
  export { buildStateFlowSectionView, createStateFlowTelegramAdapter, formatStateFlowSectionLabel, loadStateFlowTelegramModules, STATE_FLOW_TELEGRAM_ID, type StateFlowTelegramAdapter, type StateFlowTelegramButton, type StateFlowTelegramCallbackContext, type StateFlowTelegramControlResult, type StateFlowTelegramLoader, type StateFlowTelegramModules, type StateFlowTelegramPort, type StateFlowTelegramSectionContext, type StateFlowTelegramSectionModule, type StateFlowTelegramSnapshot, type StateFlowTelegramView } from "./lib/telegram.ts";
17
- export { advanceTemporalState, readTemporalState, type TemporalState } from "./lib/temporal.ts";
17
+ export { advanceTemporalState, readTemporalState, temporalScopeRevisions, type ScopeRevisions, type TemporalState } from "./lib/temporal.ts";
18
18
  export { parseStateReadPath, readStatePath, type StateReadQuery, type StateReadResult } from "./lib/query.ts";
@@ -3,7 +3,7 @@ export { classifyArtifactCompilationNeed, compileArtifact, hashArtifactSource, i
3
3
  export { buildContinuationCandidates, discoverNativeSessionHeaders, inspectStateFlowContinuationProvenance, readNativeSessionHeader, recommendContinuationFromProvenance, resolveContinuationStartup } from "./lib/continuation.js";
4
4
  export { captureTemporalFileBases, cwdScopeKey, getDurableRepositoryRoot, isStateFlowOwnedPath, loadScopeStream, parseScopeProvenance, parseScopeStream, serializeScopeProvenance, serializeScopeStream, sessionRuntimePaths, sessionScopeKey, sessionStorageKey, temporalScopePaths, temporalStateFileUpdates } from "./lib/durable.js";
5
5
  export { default, PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME } from "./lib/extension.js";
6
- export { backupCurrentStateFlowFiles } from "./lib/git.js";
6
+ export { backupCurrentStateFlowFiles, pushCurrentStateFlowBackup } from "./lib/git.js";
7
7
  export { createAcceptedTransition, DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, projectRecentTransitionsWithLimit, validateRecentTransition } from "./lib/history.js";
8
8
  export { applyPatch, canonicalJson, hashJson, isObject, validatePatch } from "./lib/json.js";
9
9
  export { appendStateFlowDiagnostic, projectDiagnosticContent, stateFlowLogPath } from "./lib/logging.js";
@@ -13,5 +13,5 @@ export { TemporalRuntime } from "./lib/runtime.js";
13
13
  export { hasCompiledSkillArtifact, hashSkillSource, SKILL_ARTIFACT_COMPILER } from "./lib/skills.js";
14
14
  export { emptyState, isMaterializedState, isStateDocument, overlayStates, projectStateForModel, updateMaterializedArtifacts } from "./lib/state.js";
15
15
  export { buildStateFlowSectionView, createStateFlowTelegramAdapter, formatStateFlowSectionLabel, loadStateFlowTelegramModules, STATE_FLOW_TELEGRAM_ID } from "./lib/telegram.js";
16
- export { advanceTemporalState, readTemporalState } from "./lib/temporal.js";
16
+ export { advanceTemporalState, readTemporalState, temporalScopeRevisions } from "./lib/temporal.js";
17
17
  export { parseStateReadPath, readStatePath } from "./lib/query.js";
@@ -10,6 +10,7 @@ export interface ScopeStreamSources {
10
10
  /** Read-only activation eligibility for one canonical CWD scope. */
11
11
  export declare function hasCwdMaterialization(cwd: string, repositoryRoot: string): boolean;
12
12
  export interface ScopeTemporalMetadata {
13
+ revision: number;
13
14
  checkpoint: ScopeStream["checkpoint"]["through"];
14
15
  patches: ScopeStream["patches"][number]["transition"][];
15
16
  }
@@ -36,6 +36,7 @@ export function serializeScopeStream(stream, scope, cwdIdentity) {
36
36
  checkpoint: `${canonicalJson(stream.checkpoint.state)}\n`,
37
37
  patches: stream.patches.map((record) => `${canonicalJson(record.patch)}\n`).join(""),
38
38
  temporal: {
39
+ revision: stream.revision,
39
40
  checkpoint: structuredClone(stream.checkpoint.through),
40
41
  patches: stream.patches.map((record) => structuredClone(record.transition)),
41
42
  },
@@ -92,6 +93,9 @@ export function classifyScopeStream(checkpointSource, patchesSource, scope, expe
92
93
  if (boundaries.length !== patches.length)
93
94
  throw new Error(`State Flow ${scope} temporal metadata does not match its semantic tail`);
94
95
  const stream = {
96
+ // 0.17.4 and earlier did not persist scope revisions. Credit only their still-retained
97
+ // semantic tail; discarded ancestry cannot be reconstructed without inventing history.
98
+ revision: temporal.revision === undefined ? patches.length : temporal.revision,
95
99
  checkpoint: { through: temporal.checkpoint, state: checkpoint },
96
100
  patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
97
101
  };
@@ -159,7 +163,7 @@ export function serializeScopeMetadata(registry, stream, scope, cwdIdentity, exi
159
163
  const sources = serializeScopeStream(stream, scope, cwdIdentity);
160
164
  const value = {
161
165
  ...existing,
162
- version: 1,
166
+ version: 2,
163
167
  ...(registry === undefined ? {} : { artifacts: serializeArtifactProvenanceRegistry(registry) }),
164
168
  temporal: sources.temporal,
165
169
  ...(scope === "cwd" ? { owner: { cwd: resolve(cwdIdentity) } } : {}),
@@ -168,7 +172,7 @@ export function serializeScopeMetadata(registry, stream, scope, cwdIdentity, exi
168
172
  }
169
173
  /** Compatibility serializer retained for metadata-only callers. */
170
174
  export function serializeScopeProvenance(registry) {
171
- return `${canonicalJson({ version: 1, artifacts: serializeArtifactProvenanceRegistry(registry) })}\n`;
175
+ return `${canonicalJson({ version: 2, artifacts: serializeArtifactProvenanceRegistry(registry) })}\n`;
172
176
  }
173
177
  function parseMetadataDocument(source, label) {
174
178
  if (source === undefined)
@@ -189,7 +193,7 @@ export function parseScopeProvenance(source, path) {
189
193
  const value = parseMetadataDocument(source, `State Flow provenance file: ${path}`);
190
194
  if (Object.keys(value).length === 0)
191
195
  return {};
192
- if (value.version !== 1)
196
+ if (value.version !== 1 && value.version !== 2)
193
197
  throw new Error(`Invalid State Flow provenance document: ${path}`);
194
198
  if (!Object.hasOwn(value, "artifacts"))
195
199
  return {};
@@ -12,7 +12,7 @@ import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, pa
12
12
  import { readNativeSessionHeader } from "./continuation.js";
13
13
  import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
14
14
  import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
15
- import { backupCurrentStateFlowFiles } from "./git.js";
15
+ import { backupCurrentStateFlowFiles, pushCurrentStateFlowBackup } from "./git.js";
16
16
  import { projectRecentTransitionsWithLimit } from "./history.js";
17
17
  import { isObject, presentationJson, sameJson } from "./json.js";
18
18
  import { StateFlowDiagnosticWriter, stateFlowLogPath } from "./logging.js";
@@ -26,6 +26,7 @@ import { emptySnapshot, migrationFailure } from "./snapshot.js";
26
26
  import { emptyState, overlayStates, projectStateForModel } from "./state.js";
27
27
  import { compactStatus, detailedStatus, STATUS_KEY } from "./status.js";
28
28
  import { createStateFlowTelegramAdapter } from "./telegram.js";
29
+ import { temporalScopeRevisions } from "./temporal.js";
29
30
  import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition } from "./transition.js";
30
31
  export { formatPatchStateArguments };
31
32
  export const PATCH_STATE_TOOL_NAME = "patch_state";
@@ -59,7 +60,7 @@ export default function stateFlowExtension(pi, options = {}) {
59
60
  let activeContext;
60
61
  let rehydrationPhase;
61
62
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
62
- const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => activeContext?.ui.notify(message, "warning"));
63
+ const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
63
64
  let backupPending = false;
64
65
  const skillReads = new SkillReadTracker(hashSkillSource, (path) => activeContext
65
66
  ? registeredSkillResolver(activeContext.cwd, pi.getCommands())(path)
@@ -72,6 +73,14 @@ export default function stateFlowExtension(pi, options = {}) {
72
73
  function sessionAddress(ctx) {
73
74
  return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
74
75
  }
76
+ function notifyActiveContext(message) {
77
+ try {
78
+ activeContext?.ui.notify(message, "warning");
79
+ }
80
+ catch {
81
+ // An asynchronous push attempt can outlive the Pi context that launched it.
82
+ }
83
+ }
75
84
  function createRuntime(ctx) {
76
85
  return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
77
86
  }
@@ -106,8 +115,13 @@ export default function stateFlowExtension(pi, options = {}) {
106
115
  if ("boundary" in checkpoint)
107
116
  branchStartsWithoutRuntime = false;
108
117
  }
118
+ function scopeRevisions() {
119
+ return runtime?.view
120
+ ? temporalScopeRevisions(runtime.view)
121
+ : { global: 0, cwd: 0, session: 0 };
122
+ }
109
123
  function updateUi(ctx) {
110
- ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, (color, text) => ctx.ui.theme.fg(color, text)));
124
+ ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
111
125
  }
112
126
  function clearRunTransient() {
113
127
  responseAwaitingReconciliation = false;
@@ -179,6 +193,7 @@ export default function stateFlowExtension(pi, options = {}) {
179
193
  if (Object.keys(removals).length > 0) {
180
194
  const stage = stageAtomicScopePatches(scopeStates, removals, [], runtime.causalBasis());
181
195
  commitStage(stage, ctx, false);
196
+ updateUi(ctx);
182
197
  }
183
198
  refreshArtifactHints();
184
199
  }
@@ -191,12 +206,15 @@ export default function stateFlowExtension(pi, options = {}) {
191
206
  function refreshTelegramStateView(scope) {
192
207
  if (scope === "session" || scope === "effective")
193
208
  assertSelectedBranchAvailable();
194
- if (runtime?.view)
195
- return;
196
- if (!activeContext)
197
- throw new Error("State Flow is not attached to an active session yet");
198
- runtime ??= createRuntime(activeContext);
199
- runtime.loadPassive();
209
+ if (!runtime?.view) {
210
+ if (!activeContext)
211
+ throw new Error("State Flow is not attached to an active session yet");
212
+ runtime ??= createRuntime(activeContext);
213
+ runtime.loadPassive();
214
+ }
215
+ else if (scope !== "session") {
216
+ runtime.refreshShared();
217
+ }
200
218
  installScopeStates();
201
219
  }
202
220
  function syncStateFlowTools() {
@@ -288,6 +306,7 @@ export default function stateFlowExtension(pi, options = {}) {
288
306
  head: structuredClone(view.lineage.at(-1)),
289
307
  historyDepth: view.lineage.length - 1,
290
308
  tailCounts: { global: view.scopes.global.patches.length, cwd: view.scopes.cwd.patches.length, session: view.scopes.session.patches.length },
309
+ revisions: temporalScopeRevisions(view),
291
310
  } }),
292
311
  staleArtifacts,
293
312
  ...(durableStateError === undefined ? {} : { durableStateError }),
@@ -689,6 +708,7 @@ export default function stateFlowExtension(pi, options = {}) {
689
708
  snapshot: () => ({
690
709
  enabled: snapshot.config.enabled,
691
710
  step: snapshot.meta.step,
711
+ revisions: scopeRevisions(),
692
712
  bootstrap: snapshot.meta.bootstrap === true,
693
713
  startPending: telegramStartPending,
694
714
  }),
@@ -699,6 +719,7 @@ export default function stateFlowExtension(pi, options = {}) {
699
719
  : scopeStates[scope];
700
720
  return { ...projectModelState(selected), lazy: structuredClone(selected.lazy) };
701
721
  },
722
+ revisions: () => scopeRevisions(),
702
723
  canStartNow: () => activeContext === undefined || activeContext.isIdle(),
703
724
  start: () => {
704
725
  if (!activeContext)
@@ -907,8 +928,16 @@ export default function stateFlowExtension(pi, options = {}) {
907
928
  return;
908
929
  backupPending = false;
909
930
  try {
910
- if (existsSync(join(repositoryRoot, ".git")))
931
+ if (existsSync(join(repositoryRoot, ".git"))) {
911
932
  backupCurrentStateFlowFiles(repositoryRoot);
933
+ const pushSessionId = sessionAddress(ctx).id;
934
+ const pushCwd = ctx.cwd;
935
+ void pushCurrentStateFlowBackup(repositoryRoot).catch((error) => {
936
+ const message = error instanceof Error ? error.message : String(error);
937
+ diagnosticWriter.record(pushSessionId, pushCwd, message, "publication-conflict");
938
+ notifyActiveContext(`State Flow accepted canonical state; Git backup push failed: ${message}`);
939
+ });
940
+ }
912
941
  }
913
942
  catch (error) {
914
943
  const message = error instanceof Error ? error.message : String(error);
@@ -951,10 +980,11 @@ export default function stateFlowExtension(pi, options = {}) {
951
980
  runAnchorTimestamp = undefined;
952
981
  restoreActiveBranch(ctx);
953
982
  });
954
- pi.on("session_shutdown", (_event, ctx) => {
983
+ pi.on("session_shutdown", (_event, _ctx) => {
955
984
  telegramStartPending = false;
956
985
  compactionStopped = true;
957
986
  completedRunAccepted = false;
987
+ activeContext = undefined;
958
988
  telegram.dispose();
959
989
  });
960
990
  }
@@ -1,2 +1,8 @@
1
1
  /** Commit only current State Flow-owned files; never changes canonical acceptance. */
2
2
  export declare function backupCurrentStateFlowFiles(repositoryRoot: string): string | undefined;
3
+ /** Push the current backup commit to its explicitly configured branch remote without blocking settlement. */
4
+ export declare function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{
5
+ commit: string;
6
+ remote: string;
7
+ ref: string;
8
+ } | undefined>;
@@ -2,11 +2,16 @@
2
2
  import { closeSync, constants, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join, relative, resolve, sep } from "node:path";
5
- import { spawnSync } from "node:child_process";
5
+ import { spawn, spawnSync } from "node:child_process";
6
6
  import { captureOwnedFileBases, isStateFlowOwnedPath } from "./durable.js";
7
7
  import { acquirePublicationLock, withStoragePublicationLock } from "./storage.js";
8
8
  const GIT_TIMEOUT_MS = 15_000;
9
9
  const STATE_FLOW_COMMIT_TRAILER = "State-Flow-Durable: v1";
10
+ function redactGitDiagnostic(value) {
11
+ return value
12
+ .replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1***@")
13
+ .replace(/([?&](?:access_token|auth|password|token)=)[^&\s]+/gi, "$1***");
14
+ }
10
15
  function git(repositoryRoot, args, options = {}) {
11
16
  const result = spawnSync("git", ["-C", repositoryRoot, ...args], {
12
17
  encoding: "utf8",
@@ -124,6 +129,25 @@ function currentBranchRef(repositoryRoot) {
124
129
  throw new Error("State Flow backup requires an attached Git branch");
125
130
  return result.stdout.trim();
126
131
  }
132
+ function configuredPushDestination(repositoryRoot) {
133
+ const branchRef = currentBranchRef(repositoryRoot);
134
+ const branch = branchRef.slice("refs/heads/".length);
135
+ const configuredRemote = git(repositoryRoot, ["config", "--get", `branch.${branch}.remote`], { allowFailure: true });
136
+ if (configuredRemote.status > 1)
137
+ throw new Error(configuredRemote.stderr || "Cannot inspect State Flow backup remote configuration");
138
+ if (configuredRemote.status !== 0 || configuredRemote.stdout.trim().length === 0)
139
+ return undefined;
140
+ const remote = configuredRemote.stdout.trim();
141
+ if (remote === ".")
142
+ throw new Error("State Flow backup replication requires a non-local Git remote");
143
+ const configuredMerge = git(repositoryRoot, ["config", "--get", `branch.${branch}.merge`], { allowFailure: true });
144
+ if (configuredMerge.status > 1)
145
+ throw new Error(configuredMerge.stderr || "Cannot inspect State Flow backup branch configuration");
146
+ const ref = configuredMerge.status === 0 && configuredMerge.stdout.trim().length > 0
147
+ ? configuredMerge.stdout.trim()
148
+ : branchRef;
149
+ return { remote, ref };
150
+ }
127
151
  function commitCurrentOwnedFiles(repositoryRoot, expectedHead) {
128
152
  const branchRef = currentBranchRef(repositoryRoot);
129
153
  if (currentHead(repositoryRoot) !== expectedHead)
@@ -181,3 +205,72 @@ function commitCurrentOwnedFiles(repositoryRoot, expectedHead) {
181
205
  export function backupCurrentStateFlowFiles(repositoryRoot) {
182
206
  return withBackupLock(repositoryRoot, (root) => commitCurrentOwnedFiles(root, currentHead(root)));
183
207
  }
208
+ /** Push the current backup commit to its explicitly configured branch remote without blocking settlement. */
209
+ export function pushCurrentStateFlowBackup(repositoryRoot) {
210
+ return new Promise((resolvePush, rejectPush) => {
211
+ let root;
212
+ let commit;
213
+ let destination;
214
+ try {
215
+ root = assertRepositoryRoot(repositoryRoot);
216
+ commit = currentHead(root);
217
+ destination = configuredPushDestination(root);
218
+ }
219
+ catch (error) {
220
+ rejectPush(error);
221
+ return;
222
+ }
223
+ if (commit === undefined || destination === undefined) {
224
+ resolvePush(undefined);
225
+ return;
226
+ }
227
+ const child = spawn("git", ["-C", root, "push", "--porcelain", "--", destination.remote, `${commit}:${destination.ref}`], {
228
+ detached: process.platform !== "win32",
229
+ stdio: ["ignore", "ignore", "pipe"],
230
+ windowsHide: true,
231
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" },
232
+ });
233
+ let stderr = "";
234
+ let failure;
235
+ let settled = false;
236
+ function terminate(error) {
237
+ failure ??= error;
238
+ if (child.exitCode !== null || child.signalCode !== null) {
239
+ child.stderr?.destroy();
240
+ return;
241
+ }
242
+ if (!child.pid)
243
+ return;
244
+ try {
245
+ if (process.platform === "win32")
246
+ child.kill("SIGKILL");
247
+ else
248
+ process.kill(-child.pid, "SIGKILL");
249
+ }
250
+ catch {
251
+ // Keep waiting for close: signalling failure is not proof that the process ended.
252
+ }
253
+ }
254
+ const timeout = setTimeout(() => terminate(new Error(`Git backup push timed out after ${GIT_TIMEOUT_MS}ms`)), GIT_TIMEOUT_MS);
255
+ function finish(error) {
256
+ if (settled)
257
+ return;
258
+ settled = true;
259
+ clearTimeout(timeout);
260
+ if (error)
261
+ rejectPush(error);
262
+ else
263
+ resolvePush({ commit: commit, ...destination });
264
+ }
265
+ child.stderr?.on("data", (chunk) => { stderr = (stderr + chunk.toString("utf8")).slice(-1000); });
266
+ child.on("error", (error) => {
267
+ failure ??= error;
268
+ if (!child.pid)
269
+ finish(error);
270
+ });
271
+ child.once("exit", () => { if (failure)
272
+ child.stderr?.destroy(); });
273
+ child.once("close", (code, endedBy) => finish(failure ?? (code === 0 ? undefined
274
+ : new Error(`Git backup push failed (${endedBy ?? code}): ${redactGitDiagnostic(stderr.trim()) || "no diagnostic output"}`))));
275
+ });
276
+ }
@@ -47,7 +47,7 @@ READ: Use read_state for concrete scope/history gaps. lazy_navigation lists boun
47
47
 
48
48
  WRITE: patch_state is the sole model-authored semantic mutation mechanism. Supply global/cwd/session patches in any combination; all supplied scopes are validated and durably accepted as one atomic transition. Call it alone in an assistant response, then continue only after its acknowledgement.
49
49
 
50
- RESPONSE: Ordinary assistant completion needs no finalization patch. Runtime reconciles the accepted non-empty answer into response at turn_end without another inference.
50
+ RESPONSE: Ordinary assistant completion needs no finalization patch. At turn_end, runtime stores the exact accepted answer; empty becomes response "".
51
51
 
52
52
  INTENTS: Keep chosen actions; detail may stay lazy. State refs use {"$ref":"cwd.lazy.plan"} or \`$cwd.lazy.plan\` in text. Resolve only when needed; infer no authority, hydration, execution, or completion. If that resolution proves a dangling state ref, fix/drop it in owning text; never scan for broken refs.
53
53
 
@@ -78,11 +78,8 @@ export function finalizedAssistantResponse(message) {
78
78
  if (message.content.some((block) => block.type === "toolCall")) {
79
79
  throw new Error("Accepted State Flow response cannot contain a tool call");
80
80
  }
81
- const response = message.content
81
+ return message.content
82
82
  .filter((block) => block.type === "text")
83
83
  .map((block) => block.text)
84
84
  .join("");
85
- if (response.trim().length === 0)
86
- throw new Error("Finalized State Flow response must contain non-empty text");
87
- return response;
88
85
  }
@@ -74,6 +74,8 @@ export declare class TemporalRuntime {
74
74
  * session or runtime files also fails closed under the existing race rule.
75
75
  */
76
76
  private reconcileSharedDrift;
77
+ /** Refresh live shared scopes in memory without publishing or advancing private semantic history. */
78
+ refreshShared(): boolean;
77
79
  /** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
78
80
  acceptRestoredOrigin(snapshot: Snapshot): RuntimePublication;
79
81
  publish(snapshot: Snapshot, semantic?: boolean, accepted?: AcceptedTransition, options?: {
@@ -402,6 +402,17 @@ export class TemporalRuntime {
402
402
  throw targetScopeConflict(targets);
403
403
  return { view: reconciledView(), base: captured, provenance };
404
404
  }
405
+ /** Refresh live shared scopes in memory without publishing or advancing private semantic history. */
406
+ refreshShared() {
407
+ if (!this.view || !this.base)
408
+ throw new Error("State Flow shared refresh requires a selected temporal runtime");
409
+ const before = this.view;
410
+ const reconciled = this.reconcileSharedDrift(new Set());
411
+ this.view = reconciled.view;
412
+ this.base = reconciled.base;
413
+ this.provenanceByScope = reconciled.provenance;
414
+ return before !== this.view;
415
+ }
405
416
  /** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
406
417
  acceptRestoredOrigin(snapshot) {
407
418
  if (!this.restoredOriginPending)