@llblab/pi-kit 0.11.1 → 0.11.3
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 +9 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +3 -3
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -7
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +15 -8
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +46 -15
- package/node_modules/@llblab/pi-state-flow/lib/{terminal.ts → protocol.ts} +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +14 -5
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +96 -3
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -4
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/api/sections.ts +2 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/sections.md +9 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -4
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +5 -14
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +9 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +21 -0
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +87 -3
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +9 -0
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +25 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.11.3 - 2026-09-14
|
|
6
|
+
|
|
7
|
+
- `Telegram Rich Sections And Queue Continuation`: Advances the exact Telegram pin to `0.45.9`, adding callback-targeted Native Rich Messages for companion sections and binding `/next` notices to the exact dispatchable queue item across concurrent readiness, admission, mutation, reordering, clearing, and transport changes.
|
|
8
|
+
- `State Flow Inspection`: Advances the exact State Flow pin to `0.11.0`, adding read-only Global, CWD, Session, and Effective state inspection through the Telegram submenu with bounded collapsible Native Rich Message rendering. The package set, resource inventory, and explicit load order remain unchanged.
|
|
9
|
+
|
|
10
|
+
## 0.11.2 - 2026-09-13
|
|
11
|
+
|
|
12
|
+
- `State Flow Compaction And Resilience`: Advances the exact State Flow pin to `0.10.2`, replacing byte-based early-compaction readiness with public context-token usage, retaining the complete latest accepted iteration after shortening, accepting unambiguous non-terminal `final:false`, and strengthening bounded memory stewardship. The package set, resource inventory, and explicit load order remain unchanged.
|
|
13
|
+
|
|
5
14
|
## 0.11.1 - 2026-09-13
|
|
6
15
|
|
|
7
16
|
- `State Flow Filesystem Recovery`: Advances the exact State Flow pin to `0.10.1`, adding transactional self-healing when a complete live global or CWD checkpoint pair disappears, exact private-session reconstruction from retained Git evidence, and precise fail-closed handling for partial or unsafe storage loss. The package set, resource inventory, and explicit load order remain unchanged.
|
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
14
14
|
| [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.1.1` | Isolated nested Pi TUI with explicitly selected extensions |
|
|
15
15
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
|
|
16
16
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
|
|
17
|
-
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.
|
|
17
|
+
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.11.0` | Durable scoped state with native session compaction, exact publication, and read-only Telegram inspection |
|
|
18
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.9` | Telegram companion with exact queues, Rich section messages, 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.
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
- 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.
|
|
26
26
|
- 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; require exact scalar-string commit targets/confirmations without coercion, 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. Revalidate dead lease ownership under the existing queue writer lock before removal; fresh exclusive creation and live self-PID/token release remain independent of that gate. Preserve malformed and non-regular lease records. 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.
|
|
27
27
|
- Own asynchronous pushes per extension generation with a 15-second attempt budget and at most two seconds of shutdown waiting. On `session_shutdown`, cancel owned children and permanently fence old-generation queue writes and relaunch. Cancellation or elapsed waiting is not child-exit evidence: keep the lease until exit or proven spawn failure, and warn when cleanup remains unconfirmed. These deadlines govern asynchronous replication, not semantic acceptance or the compatibility synchronous mode.
|
|
28
|
-
- Register `patch_state` as the sole model-authored semantic mutation protocol.
|
|
28
|
+
- Register `patch_state` as the sole model-authored semantic mutation protocol. Its canonical model-facing grammar accepts optional fixed `global`, `cwd`, and `session` semantic patches plus optional `final:true`; require at least one scope or `final:true`, reject unknown fields, empty supplied scopes, material no-ops, and the retired `{scope, patch}` / `unchanged` grammar. As an unadvertised graceful-degradation layer, normalize only unambiguous Boolean-like `final` primitives before schema validation, treat false-like values as non-terminal intent, and accept a false-only call as an inert no-op without a transition or diagnostic; never apply raw JavaScript truthiness. 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`.
|
|
29
29
|
- 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. 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.
|
|
30
30
|
- 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.
|
|
31
31
|
- 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.
|
|
32
32
|
- 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. 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.
|
|
33
33
|
- 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.
|
|
34
34
|
- Treat each successful `SKILL.md` read as CWD artifact acquisition using the finalized tool-execution arguments after mutable interception: require a non-empty compiler output in the next `patch_state` call under `cwd.artifacts[exactReadPath]` with `description`, `kind: "skill"`, and a flexible non-empty `compilation` object; hash the executed source bytes and record runtime-owned `sourceHash` plus `skill-artifact-v1` `compilerRevision` in the CWD provenance registry; reject missing, unhashable, malformed, or forged freshness data; replace the complete prior Skill artifact and its provenance entry on refresh so obsolete evidence cannot survive. `contract.compiled_skills` is retired and rejected; migrate useful legacy entries into artifacts while preserving behavior and marking fallback hashes unverified when the source is unavailable.
|
|
35
|
-
- Make
|
|
35
|
+
- 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. At a completed feature/release/campaign, project switch, or active-version change, require one bounded scoped reconciliation before terminal completion; use targeted `read_state` when ownership is not visible, then write and verify the destination before deleting and verifying the source. Global is only established cross-project/user/environment knowledge, CWD is reusable project truth, and session is branch/run continuation.
|
|
36
36
|
- Accept omitted semantic fields inside each supplied scope patch, but reject empty supplied scopes; when no state change is needed require explicit `{final:true}` without inventing bookkeeping. This final-only call changes only ephemeral terminal eligibility and creates no semantic transition, identity, temporal step, or Git commit. 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.
|
|
37
37
|
- Agent-level opt-in `logging` appends local JSONL diagnostics for rejected `patch_state` attempts and preserved drafts plus fallback turns while turn resolution remains pending. Every rejected call retains its exact attempted arguments plus the precise error and, when available, tool identity, call id, resolution attempt, and terminal-eligibility state; successful patches 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 resolution, enablement, or accepted state.
|
|
38
38
|
- 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.
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
- Render compact status as accent `state-flow` plus dim `#<step>`. `/state-flow-status` must distinguish session config/meta from semantic temporal materialization; show the CWD and session keys, step, active temporal head and State Flow revision, available hot-history depth, per-scope artifact/tail counts, discovered global Markdown stale reasons, and pending publication; and render only the effective global → CWD → session materialization as semantic JSON, without reading or dumping source bodies beyond path/hash discovery needed for freshness diagnostics. Distinguish selected retained tail counts from active history depth; inherited tails may predate the origin. When temporal materialization is unavailable, report unknown counts/freshness and unavailable state rather than inventing empty projections.
|
|
44
44
|
- Explicit `/state-flow-start` creates the State Flow directory when missing and returns after locally usable runtime acceptance for normal `turn-end`/`off` policy. If Git is installed, initialize an exact-root Git repository when needed, including a populated file-only store, preserving all existing bytes and unrelated files; an ancestor repository is not a valid substitute. Skip full predecessor-format migration planning only when all exact legacy snapshot names are absent, and defer Markdown freshness discovery until before the next enabled inference. If Git is absent, use file persistence. Manual-mode startup/status/restore do not initialize Git; configured automatic start of a genuinely new session uses the same initialization as explicit start. Never create external accounts, remote repositories, credentials, or remote configuration; those remain operator-owned. Never auto-import, delete, or reset files/history in a previous Knowledge-backed store; old branch revisions require their original Git history to remain available in the selected store. `/state-flow-start` must initialize missing global, CWD, and current-session checkpoint/tail pairs plus session config/meta through compare-and-swap publication when required, enable only the current session branch, and bootstrap prior conversation when needed. Explicit start on a proven pre-runtime branch (no checkpoint or an ordinary-disabled marker) establishes an empty session origin rather than importing a later same-session layer; validate existing runtime identity, retain shared streams unchanged, and preserve later branch data in cold Git history. Ordinary new sessions remain manual unless agent-level `autoStart` is true; CWD materialization alone grants no automatic activation. 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 own config and temporal lineage, regardless of the global flag.
|
|
45
45
|
- `/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. 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 and private State Flow validation feedback; idle Stop does not retain the completed run. 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.
|
|
46
|
-
- After
|
|
46
|
+
- 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 user iteration, store only revision/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. Skip prefixes containing foreign custom 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.
|
|
47
47
|
- 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.
|
|
48
48
|
- Do not claim strict boundedness for state, the current run trajectory, the turn specification, or the external full trace.
|
|
49
49
|
- 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.
|
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Completed release work belongs in [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
-
## 0.10.1: Filesystem self-healing and graceful degradation
|
|
6
|
-
|
|
7
|
-
- **Outcome:** Treat safely interpretable absence as recoverable or optional while keeping partial, malformed, contradictory, and authority-losing evidence fail-closed at the smallest affected capability. [`docs/filesystem-recovery.md`](docs/filesystem-recovery.md) owns the durable cohort classification.
|
|
8
|
-
- **Accepted candidate:** Shared Git/file repair, exact session authority, provenance/external degradation, operational persistence, concurrent CAS and native Pi response/reload/resume witnesses are complete. A focused release review found and corrected repeated stale-target refusal; the runtime now refreshes the empty basis after one conflict and accepts a later patch through ordinary publication.
|
|
9
|
-
- **Validation:** The complete 0.84.4 repository profile passes 442/442 with typecheck/import-check. On the supported 0.85.1 stack, all 16 affected codec/runtime witnesses, the real Pi lifecycle witness and typecheck pass. Package 0.10.1 contains 46 files including the recovery guide and excludes tests/benchmarks; context/DAG validation report zero errors.
|
|
10
|
-
- [ ] **Release gate:** Commit, push, tag, npm/GitHub publication, and any Pi Kit pin update require separate explicit release authorization.
|
|
11
|
-
|
|
12
5
|
## Candidate evolution
|
|
13
6
|
|
|
14
7
|
- [ ] **Read-only global bootstrap layer:** Project the existing global materialization into enabled and non-enabled sessions by default, so even one-shot work starts carrying established cross-project facts, preferences, routing, and conventions. The global scope is the highest-value, lowest-cost half of the memory, while writing carries the protocol and curation tax; making the read side unconditional gives continuity without enabling mutation.
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.11.0: Telegram state inspection
|
|
8
|
+
|
|
9
|
+
- `Telegram inspection`: The State Flow submenu now offers `👁 Show state` with Global, CWD, Session, and Effective choices. Selecting one scope sends exactly that projected materialized slice as a standalone Telegram Native Rich Message whose semantic fields use collapsible `details` and JSON `pre` blocks; inspection is read-only and the optional adapter remains fail-open without a compatible pi-telegram membrane.
|
|
10
|
+
|
|
11
|
+
## 0.10.2: Token-guided compaction and patch resilience
|
|
12
|
+
|
|
13
|
+
- `Compaction readiness`: Early State Flow compaction now uses Pi's public context-token estimate with a 24,000-token floor instead of serialized-entry bytes, skipping unknown or short contexts without invoking native compaction. Successful shortening retains the complete latest accepted user iteration rather than only its final answer, while durable state and append-only session history remain intact.
|
|
14
|
+
- `Patch resilience`: `patch_state` now quietly normalizes a bounded Boolean-like compatibility set for `final`. Explicit false can accompany an atomic semantic patch or stand alone as an inert non-terminal no-op without clearing eligibility, creating history, or producing an invalid-patch diagnostic; model-facing guidance still teaches only omission and `final:true`, and ambiguous values remain invalid.
|
|
15
|
+
- `Memory stewardship`: Runtime guidance, tool prompts, project protocol, architecture/usage docs, and the bundled memory Skill now require narrowest-scope placement and touched-branch cleanup on ordinary handoffs, plus one bounded ownership/obsolescence reconciliation when a feature, release, campaign, project, or active version changes. Scope migration remains targeted, verified, non-background, and destination-first.
|
|
16
|
+
|
|
7
17
|
## 0.10.1: Filesystem self-healing and graceful degradation
|
|
8
18
|
|
|
9
19
|
- `Shared scopes`: A wholly absent live global/CWD checkpoint-tail pair is now distinct from partial storage. Untouched publication adopts empty current reality at a fresh origin and transactionally materializes the canonical pair without resurrecting selected values; a patch targeting the disappeared scope fails precisely for reinference. Git/file backends retain publication locks, CAS, full cold history and concurrent-writer exclusion.
|
|
@@ -53,7 +53,7 @@ Continue working normally. The agent receives the state protocol and uses `patch
|
|
|
53
53
|
|
|
54
54
|
State Flow is **opt-in**. Starting in an existing conversation keeps its active context for one migration run. To enable genuinely new sessions automatically, set `"autoStart": true` in the optional [configuration](docs/usage.md#configuration).
|
|
55
55
|
|
|
56
|
-
With `pi-telegram` installed, its main menu also exposes the same Start/Stop controls.
|
|
56
|
+
With a compatible `pi-telegram` installed, its main menu also exposes the same Start/Stop controls and a `👁 Show state` chooser for Global, CWD, Session, or Effective memory. Selecting one scope sends exactly that materialized slice as a standalone native Rich Message with collapsible semantic fields; inspection is read-only and does not create a State Flow transition.
|
|
57
57
|
|
|
58
58
|
## What carries forward
|
|
59
59
|
|
|
@@ -89,7 +89,7 @@ The existing native passive-stop marker stores the stop timestamp and an optiona
|
|
|
89
89
|
|
|
90
90
|
The current user specification stays at user authority and appears only in synthetic user runtime context. State is fallible assistant-produced data. Completed trajectories leave model context at user-run boundaries, while Pi's full JSONL trace remains inspectable.
|
|
91
91
|
|
|
92
|
-
After
|
|
92
|
+
After an accepted non-bootstrap run settles with no queued input, State Flow may request native manual compaction under a generation-private marker when public `getContextUsage()` reports at least 24,000 tokens. This token signal replaces the former serialized-byte proxy and provides a modest margin above Pi's default 20,000-token retained suffix; Pi still owns preparation and may benignly decline when custom settings leave no compactable prefix. The extension supplies no model-generated summary or state body: it keeps the complete latest accepted user iteration and records the exact durable revision/step in compaction details. `buildContextEntries()` then omits the older completed prefix for active context and resume rendering while the append-only JSONL/tree remains intact. Unknown or smaller usage, foreign custom context in the removed prefix, stale selection, Stop/bootstrap/fallback/error/abort and pending input do not produce this boundary. User manual and native threshold/overflow compaction remain unmodified; in-progress work not yet accepted into State Flow stays under Pi's native compaction contract.
|
|
93
93
|
|
|
94
94
|
The Pi adapter passes its raw cached scope overlay to `runtimeContextMessage`, which owns model sanitization of the current state. It does not pre-project that input. After anchor selection, `currentRunTrajectory` selects retained messages into one array without copying discarded ordinary prefixes: foreign custom messages survive at any position, while ordinary messages survive only from the selected run anchor and State Flow's private feedback is excluded. The necessary foreign-context scan and Pi's earlier native-message clone remain history-dependent. See [context-cost evidence](performance.md#context-projection-and-trajectory-selection).
|
|
95
95
|
|
|
@@ -196,7 +196,7 @@ Both [tested Pi SDKs](compatibility.md) choose or create `SessionManager` before
|
|
|
196
196
|
|
|
197
197
|
### Model tools
|
|
198
198
|
|
|
199
|
-
`patch_state` accepts optional fixed `global`, `cwd`, and `session` semantic patches plus optional `final:true`. At least one scope or `final:true` is required. Supplied scopes contain only object-valued `artifacts`, `contract`, and `working`; omitted fields preserve their values, recursive object merge updates them, arrays/primitives replace, and nested object-key `null` deletes. Materialized null, empty supplied scopes, material no-ops, unknown top-level fields, model-authored `response`, and retired grammars are rejected. A final-only call changes ephemeral eligibility, not semantic history.
|
|
199
|
+
`patch_state` accepts optional fixed `global`, `cwd`, and `session` semantic patches plus optional `final:true`. At least one scope or `final:true` is required in the canonical model contract. Supplied scopes contain only object-valued `artifacts`, `contract`, and `working`; omitted fields preserve their values, recursive object merge updates them, arrays/primitives replace, and nested object-key `null` deletes. Materialized null, empty supplied scopes, material no-ops, unknown top-level fields, model-authored `response`, and retired grammars are rejected. A final-only true call changes ephemeral eligibility, not semantic history. Runtime quietly accepts unambiguous false-like `final` input as non-terminal intent, including an inert false-only call, but this compatibility layer is deliberately absent from model guidance.
|
|
200
200
|
|
|
201
201
|
```json
|
|
202
202
|
{"session":{"working":{"next":"Verify the corrected behavior"}},"final":true}
|
|
@@ -208,7 +208,7 @@ Both [tested Pi SDKs](compatibility.md) choose or create `SessionManager` before
|
|
|
208
208
|
{"offset":1,"scope":"cwd"}
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
-
Both tools follow branch enablement and host restrictions. The patch barrier also blocks reader siblings. These tools do not impose project schemas or state-size caps; semantic usefulness, scope choice, and compression remain model responsibilities.
|
|
211
|
+
Both tools follow branch enablement and host restrictions. The patch barrier also blocks reader siblings. These tools do not impose project schemas or state-size caps; semantic usefulness, scope choice, and compression remain model responsibilities. Every ordinary handoff reconciles touched and obviously stale visible state. Feature/release/campaign completion, project switches, and active-version changes additionally require one bounded scoped ownership and obsolescence pass: global retains only established cross-project/user/environment knowledge, CWD owns reusable project truth, and session owns branch/run continuation. Scope movement uses targeted reads and destination verification before source deletion rather than an automatic maintenance loop.
|
|
212
212
|
|
|
213
213
|
### Embedding
|
|
214
214
|
|
|
@@ -12,7 +12,7 @@ For the concept and installation, start with the [README](../README.md). This gu
|
|
|
12
12
|
- **Abort inference:** Stops generation while already accepted patches remain durable for continued work and corrected direction in the same session. It does not roll back memory or require immediate remote replication.
|
|
13
13
|
- **Stop:** Disables semantic tools and updates immediately on the selected branch; ordinary prompt composition resumes with the next user run. It preserves state and does not create a semantic transition or change automatic-start policy for future new sessions.
|
|
14
14
|
- **Continue after Stop:** The same physical session retains a frozen state handoff, any interrupted current request and tool trajectory (including late results), and post-stop conversation. Completed earlier conversation stays excluded, while other extensions' custom context survives. Reload/resume/tree preserve this projection; new/forked physical sessions do not inherit it. Active restart uses it for one bootstrap run.
|
|
15
|
-
- **Completed-history compaction:** After
|
|
15
|
+
- **Completed-history compaction:** After an accepted run settles without queued input, State Flow asks Pi for a native compaction boundary only when public context usage reaches 24,000 tokens. No extra model summary is requested; Pi keeps the complete latest user iteration—from its request through tools and final answer—in active history and retains the complete append-only JSONL/tree. On resume, native `buildContextEntries()` and TUI rendering omit the older completed prefix. Unknown or smaller usage skips the request, and custom Pi retention settings may still decline it benignly. Foreign custom context in the removed prefix, bootstrap/fallback/abort/error, Stop and pending input prevent State Flow-owned shortening; ordinary manual/threshold/overflow compaction remains native and may preserve unfinished work not yet patched into memory.
|
|
16
16
|
|
|
17
17
|
State Flow does not undo tool effects. After interruption or returning to an older branch, check the relevant workspace or external system before repeating consequential operations. Restored memory is not restored reality.
|
|
18
18
|
|
|
@@ -139,4 +139,4 @@ The agent should use sufficient materialized knowledge before rereading files. R
|
|
|
139
139
|
|
|
140
140
|
Knowledge discovery finds regular lowercase `*.md` beneath its configured root, hashes opaque bytes, and skips symlinks. Only confirmed missing Markdown paths within an available root are pruned; external/non-Markdown artifacts and state under a missing whole root are preserved. An unavailable root makes freshness unknown in status. Successful reads of stale ordinary candidates require same-path global compilation; Skill reads require CWD compilation. The runtime owns provenance: model patches cannot write or delete individual freshness fields, including legacy spellings. Existing legacy entries remain readable and semantically editable. Missing freshness evidence means unknown-but-usable, not proof that a source was acquired.
|
|
141
141
|
|
|
142
|
-
The optional `state-flow-memory` Skill handles explicit bounded curation and external promotion. It is not a background maintenance loop. Promotion must verify the destination before removing the only accepted source copy. Artifact/compiler details and model-tool contracts belong in the [architecture](architecture.md#artifact-routing).
|
|
142
|
+
The optional `state-flow-memory` Skill handles explicit bounded curation, required feature/release/project phase-boundary reconciliation, and external promotion. Ordinary handoffs clean only touched and obviously stale visible branches; the Skill supplies the fuller scoped migration procedure when a phase boundary or explicit request earns it. It is not a background maintenance loop. Promotion must verify the destination before removing the only accepted source copy. Artifact/compiler details and model-tool contracts belong in the [architecture](architecture.md#artifact-routing).
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { CompactionResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
export const STATE_FLOW_COMPACTION_SUMMARY = "State Flow accepted the completed work before this boundary. Current memory is restored from its durable revision and projected separately; use the retained native entries for subsequent work.";
|
|
4
|
-
|
|
4
|
+
/** A modest margin above Pi's default 20k retained suffix absorbs estimation drift. */
|
|
5
|
+
export const STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS = 24_000;
|
|
5
6
|
|
|
6
7
|
export interface StateFlowCompactionDetails {
|
|
7
8
|
version: 1;
|
|
@@ -29,7 +30,13 @@ function stateFlowEntry(entry: ActiveEntry): boolean {
|
|
|
29
30
|
&& entry.customType.startsWith("state-flow-");
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
export function shouldRequestStateFlowCompaction(usage: { tokens: number | null } | undefined): boolean {
|
|
34
|
+
return typeof usage?.tokens === "number"
|
|
35
|
+
&& Number.isFinite(usage.tokens)
|
|
36
|
+
&& usage.tokens >= STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Retain the complete latest accepted user iteration without hiding foreign extension context. */
|
|
33
40
|
export function planStateFlowCompaction(
|
|
34
41
|
entries: readonly ActiveEntry[],
|
|
35
42
|
revision: string,
|
|
@@ -37,12 +44,12 @@ export function planStateFlowCompaction(
|
|
|
37
44
|
): StateFlowCompactionPlan | undefined {
|
|
38
45
|
if (!/^[0-9a-f]{40,64}$/.test(revision) && !/^file:[0-9a-f]{64}$/.test(revision)) return undefined;
|
|
39
46
|
if (!Number.isSafeInteger(step) || step < 0 || entries.length === 0) return undefined;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
const terminal = entries.findLastIndex((entry) => entry.type === "message" && entry.message?.role === "assistant");
|
|
48
|
+
if (terminal < 0 || entries[terminal]?.message?.stopReason === "aborted"
|
|
49
|
+
|| entries[terminal]?.message?.stopReason === "error"
|
|
50
|
+
|| entries[terminal]?.message?.stopReason === "length") return undefined;
|
|
51
|
+
let keep = terminal;
|
|
52
|
+
while (keep >= 0 && !(entries[keep]?.type === "message" && entries[keep]?.message?.role === "user")) keep--;
|
|
46
53
|
if (keep < 0) return undefined;
|
|
47
54
|
if (entries.slice(0, keep).some((entry) => entry.type === "custom" && !stateFlowEntry(entry))) return undefined;
|
|
48
55
|
const firstKeptEntryId = entries[keep]?.id;
|
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./
|
|
6
|
+
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.ts";
|
|
7
7
|
import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation, type PassiveContinuation } from "./context.ts";
|
|
8
8
|
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
9
9
|
import { loadStateFlowConfig } from "./config.ts";
|
|
@@ -12,7 +12,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
12
12
|
import { SkillReadTracker } from "./skills.ts";
|
|
13
13
|
import { emptySnapshot, migrationFailure, persistableSnapshot, RevisionUnavailableError, type Snapshot } from "./snapshot.ts";
|
|
14
14
|
import { readNativeSessionHeader } from "./continuation.ts";
|
|
15
|
-
import { MissingSessionRuntimeError, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
15
|
+
import { MissingSessionRuntimeError, SharedScopeRemovalConflictError, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
16
16
|
import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatches, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
17
17
|
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility, type StagedScopedTransition } from "./transition.ts";
|
|
18
18
|
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
planArtifactInvalidation,
|
|
41
41
|
type ArtifactInvalidationRequest,
|
|
42
42
|
} from "./artifact.ts";
|
|
43
|
-
import { planStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
|
|
43
|
+
import { planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
|
|
44
44
|
|
|
45
45
|
export interface StateFlowExtensionOptions {
|
|
46
46
|
agentDir?: string;
|
|
@@ -56,6 +56,20 @@ export const MAX_FALLBACK_ATTEMPTS: number = 2;
|
|
|
56
56
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
57
57
|
const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
|
|
58
58
|
|
|
59
|
+
/** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
|
|
60
|
+
export function normalizePatchStateArguments(args: unknown): any {
|
|
61
|
+
if (!isObject(args) || !Object.hasOwn(args, "final")) return args;
|
|
62
|
+
const value = args.final;
|
|
63
|
+
let final: boolean;
|
|
64
|
+
if (typeof value === "boolean") final = value;
|
|
65
|
+
else if (value === 1) final = true;
|
|
66
|
+
else if (value === 0) final = false;
|
|
67
|
+
else if (typeof value === "string" && value.trim().toLowerCase() === "true") final = true;
|
|
68
|
+
else if (typeof value === "string" && value.trim().toLowerCase() === "false") final = false;
|
|
69
|
+
else return args;
|
|
70
|
+
return { ...args, final };
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
/** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
|
|
60
74
|
function separatedFailure(error: unknown): Error {
|
|
61
75
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -281,16 +295,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
281
295
|
|
|
282
296
|
function commitStage(stage: StagedScopedTransition, ctx: ExtensionContext, finalizeRun: boolean): boolean {
|
|
283
297
|
const acquiredArtifactPaths = new Set(artifactReads.successful.keys());
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
298
|
+
let committed: boolean;
|
|
299
|
+
try {
|
|
300
|
+
committed = commitScopedTransition(snapshot, scopeStates, stage, (accepted, nextSnapshot) => {
|
|
301
|
+
if (!runtime?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
302
|
+
const mode = nextSnapshot.meta.remotePublication?.mode ?? "transition";
|
|
303
|
+
const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, {
|
|
304
|
+
pushRemote: mode === "transition",
|
|
305
|
+
provenance: stage.provenanceUpdates,
|
|
306
|
+
});
|
|
307
|
+
if (publication?.commit && mode === "turn-end") turnPublicationTarget = publication.commit;
|
|
308
|
+
if (publication) recordPublication(publication, ctx);
|
|
309
|
+
}, runtime!.causalBasis(), { finalizeRun });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (error instanceof SharedScopeRemovalConflictError) installScopeStates();
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
294
314
|
if (!committed) return false;
|
|
295
315
|
installScopeStates();
|
|
296
316
|
// A preserved primary response commits mid-run; pending acquisition obligations must
|
|
@@ -773,6 +793,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
773
793
|
promptSnippet: "Atomically patch global/cwd/session; final:true permits a later turn_end",
|
|
774
794
|
promptGuidelines: [
|
|
775
795
|
"Use patch_state for durable semantic changes. Before a final answer, make the iteration terminal-eligible with final:true, optionally alongside atomic global/cwd/session patches.",
|
|
796
|
+
"Use patch_state as reconciliation, not append-only notes: place new knowledge at the narrowest valid scope and remove superseded or completed state from touched branches.",
|
|
776
797
|
"Call patch_state alone in an assistant response; after its acknowledgement, further reasoning, tools, and later patch_state calls remain allowed.",
|
|
777
798
|
],
|
|
778
799
|
executionMode: "sequential",
|
|
@@ -782,6 +803,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
782
803
|
session: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional session semantic patch" })),
|
|
783
804
|
final: Type.Optional(Type.Boolean({ description: "Set exactly true to permit this iteration to finish at a later turn_end" })),
|
|
784
805
|
}, { additionalProperties: false }),
|
|
806
|
+
prepareArguments: normalizePatchStateArguments,
|
|
785
807
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
786
808
|
try {
|
|
787
809
|
if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
|
|
@@ -791,7 +813,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
791
813
|
for (const key of Object.keys(params)) {
|
|
792
814
|
if (!allowed.has(key)) throw new Error(`patch_state does not accept field ${key}`);
|
|
793
815
|
}
|
|
794
|
-
if (Object.hasOwn(params, "final") && params.final !==
|
|
816
|
+
if (Object.hasOwn(params, "final") && typeof params.final !== "boolean") throw new Error("patch_state final must be a Boolean when supplied");
|
|
795
817
|
const patches: AtomicScopePatches = {};
|
|
796
818
|
for (const scope of ["global", "cwd", "session"] as const) {
|
|
797
819
|
if (!Object.hasOwn(params, scope)) continue;
|
|
@@ -802,6 +824,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
802
824
|
}
|
|
803
825
|
const scopes = Object.keys(patches) as StateScope[];
|
|
804
826
|
if (scopes.length === 0) {
|
|
827
|
+
if (params.final === false) {
|
|
828
|
+
return { content: [{ type: "text", text: "\nState unchanged; iteration remains non-terminal." }], details: { final: false } };
|
|
829
|
+
}
|
|
805
830
|
if (params.final !== true) throw new Error('patch_state requires at least one scope patch or {"final":true}');
|
|
806
831
|
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
807
832
|
terminalEligible = true;
|
|
@@ -976,6 +1001,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
976
1001
|
bootstrap: snapshot.meta.bootstrap === true,
|
|
977
1002
|
startPending: telegramStartPending,
|
|
978
1003
|
}),
|
|
1004
|
+
state: (scope) => projectStateForModel(
|
|
1005
|
+
scope === "effective"
|
|
1006
|
+
? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
|
|
1007
|
+
: scopeStates[scope],
|
|
1008
|
+
),
|
|
979
1009
|
canStartNow: () => activeContext === undefined || activeContext.isIdle(),
|
|
980
1010
|
start: () => {
|
|
981
1011
|
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
@@ -1162,7 +1192,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1162
1192
|
}
|
|
1163
1193
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
1164
1194
|
if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap || resolutionPending
|
|
1165
|
-
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
|
|
1195
|
+
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
|
|
1196
|
+
|| !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
|
|
1166
1197
|
completedRunAccepted = false;
|
|
1167
1198
|
const plan = planStateFlowCompaction(ctx.sessionManager.buildContextEntries(), snapshot.meta.durableBase, snapshot.meta.step);
|
|
1168
1199
|
if (!plan) return;
|
|
@@ -3,7 +3,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
|
3
3
|
export type { StateDocument } from "./state.ts";
|
|
4
4
|
|
|
5
5
|
function baselineMemoryProtocol(): string {
|
|
6
|
-
return "BASELINE MEMORY: State Flow owns durable memory while enabled. Global is
|
|
6
|
+
return "BASELINE MEMORY: State Flow owns durable memory while enabled. Global is only for established cross-project/user/environment knowledge, cwd for reusable project truth, and session for branch/run continuation. Treat every patch as reconciliation rather than append-only notes: place new knowledge at the narrowest valid scope, reconsider touched branches, merge superseded fragments, and remove obsolete progress. Exclude secrets, raw history, transient progress, speculative clutter, and unsupported assertions; retain explicitly uncertain hypotheses only when they affect an open decision.";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/** The compact model-facing contract. Semantic writes never travel through terminal prose. */
|
|
@@ -31,7 +31,7 @@ ${baselineMemoryProtocol()}
|
|
|
31
31
|
|
|
32
32
|
PATCH: Fields are optional global, cwd, session semantic patches and optional final:true. At least one scope or final:true is required. Supplied scopes commit atomically; empty or materially no-op scopes must be omitted. Patches use only object-valued artifacts, contract, and working; omitted fields preserve. Never patch runtime config/meta/response. Recursive merge; arrays/primitives replace; nested null deletes. Materialized null is forbidden.
|
|
33
33
|
|
|
34
|
-
HANDOFF: Preserve active commitments, unresolved questions, consequential results, and exact continuation. Distinguish user requirements, confirmed decisions, observations, assistant conclusions, and hypotheses.
|
|
34
|
+
HANDOFF: Preserve active commitments, unresolved questions, consequential results, and exact continuation. Distinguish user requirements, confirmed decisions, observations, assistant conclusions, and hypotheses. Curate touched and obviously stale or mis-scoped visible state before every final handoff. When a feature/release/campaign closes or the active project/version changes, perform one bounded scoped reconciliation: remove obsolete prior-work state, retain only still-operative consequences, and use targeted read_state plus destination-verify-source-delete when ownership must move. Never invent memory changes or rewrite unrelated state for style.
|
|
35
35
|
|
|
36
36
|
ACQUISITION: Start from materialized state. Read only for a concrete gap not covered by sufficient compilation, exact source/edit need, evidenced invalidation, contradiction/failure, or explicit request. Changed hashes require rereading.
|
|
37
37
|
|
|
@@ -39,13 +39,22 @@ function targetScopeConflict(scopes: readonly StateScope[]): Error {
|
|
|
39
39
|
return new Error(`State Flow cannot publish the ${labels.join(" and ")} patches because the live ${labels.join(" and ")} states advanced after this transition's selected basis. Refresh or reconcile the target scopes before retrying.`);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** A targeted removed scope was deliberately adopted as empty before refusing the stale semantic patch. */
|
|
43
|
+
export class SharedScopeRemovalConflictError extends Error {
|
|
44
|
+
readonly scopes: readonly StateScope[];
|
|
45
|
+
constructor(scopes: readonly StateScope[]) {
|
|
46
|
+
const labels = scopes.map(scopeLabel);
|
|
47
|
+
super(labels.length === 1
|
|
48
|
+
? `State Flow cannot publish the ${labels[0]} patch because the live ${labels[0]} scope was removed after this transition's selected basis. Refresh or reconcile the target scope before retrying.`
|
|
49
|
+
: `State Flow cannot publish the ${labels.join(" and ")} patches because the live ${labels.join(" and ")} scopes were removed after this transition's selected basis. Refresh or reconcile the target scopes before retrying.`);
|
|
50
|
+
this.name = "SharedScopeRemovalConflictError";
|
|
51
|
+
this.scopes = Object.freeze([...scopes]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
/** Disappearance invalidates a selected write target even though untouched scopes can adopt empty reality. */
|
|
43
56
|
function removedTargetScopeConflict(scopes: readonly StateScope[]): Error {
|
|
44
|
-
|
|
45
|
-
if (labels.length === 1) {
|
|
46
|
-
return new Error(`State Flow cannot publish the ${labels[0]} patch because the live ${labels[0]} scope was removed after this transition's selected basis. Refresh or reconcile the target scope before retrying.`);
|
|
47
|
-
}
|
|
48
|
-
return new Error(`State Flow cannot publish the ${labels.join(" and ")} patches because the live ${labels.join(" and ")} scopes were removed after this transition's selected basis. Refresh or reconcile the target scopes before retrying.`);
|
|
57
|
+
return new SharedScopeRemovalConflictError(scopes);
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
function freshEmptyScopeStream(scope: StateScope, origin: string): ScopeStream {
|
|
@@ -16,6 +16,24 @@ export interface StateFlowTelegramSnapshot {
|
|
|
16
16
|
startPending: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type StateFlowTelegramScope = "global" | "cwd" | "session" | "effective";
|
|
20
|
+
|
|
21
|
+
export interface StateFlowTelegramState {
|
|
22
|
+
artifacts: Record<string, unknown>;
|
|
23
|
+
contract: Record<string, unknown>;
|
|
24
|
+
working: Record<string, unknown>;
|
|
25
|
+
response: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type StateFlowTelegramRichBlock =
|
|
29
|
+
| { type: "pre"; text: string; language?: string }
|
|
30
|
+
| { type: "details"; summary: string | { type: "bold" | "code"; text: string }; blocks: StateFlowTelegramRichBlock[]; is_open?: true };
|
|
31
|
+
|
|
32
|
+
export interface StateFlowTelegramRichMessage {
|
|
33
|
+
blocks: StateFlowTelegramRichBlock[];
|
|
34
|
+
skip_entity_detection?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
export interface StateFlowTelegramButton {
|
|
20
38
|
text: string;
|
|
21
39
|
callback_data: string;
|
|
@@ -30,6 +48,7 @@ export interface StateFlowTelegramView {
|
|
|
30
48
|
export interface StateFlowTelegramSectionContext {
|
|
31
49
|
callbackData(action: string, payload?: string): string;
|
|
32
50
|
edit(view: StateFlowTelegramView): Promise<void>;
|
|
51
|
+
openRich(message: StateFlowTelegramRichMessage): Promise<void>;
|
|
33
52
|
answerCallback(text?: string): Promise<void>;
|
|
34
53
|
}
|
|
35
54
|
|
|
@@ -61,6 +80,7 @@ export interface StateFlowTelegramControlResult {
|
|
|
61
80
|
|
|
62
81
|
export interface StateFlowTelegramPort {
|
|
63
82
|
snapshot(): StateFlowTelegramSnapshot;
|
|
83
|
+
state(scope: StateFlowTelegramScope): StateFlowTelegramState;
|
|
64
84
|
canStartNow(): boolean;
|
|
65
85
|
start(): StateFlowTelegramControlResult;
|
|
66
86
|
stop(): StateFlowTelegramControlResult;
|
|
@@ -103,10 +123,72 @@ export function buildStateFlowSectionView(
|
|
|
103
123
|
return {
|
|
104
124
|
text: [formatStateFlowSectionHeader(snapshot), "", STATE_FLOW_SECTION_HELP].join("\n"),
|
|
105
125
|
parseMode: "html",
|
|
106
|
-
replyMarkup: { inline_keyboard: [
|
|
126
|
+
replyMarkup: { inline_keyboard: [
|
|
127
|
+
[{ text: "👁 Show state", callback_data: callbackData("show-state") }],
|
|
128
|
+
[action],
|
|
129
|
+
] },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildStateFlowScopeChooser(callbackData: (action: string, payload?: string) => string): StateFlowTelegramView {
|
|
134
|
+
return {
|
|
135
|
+
text: "<b>👁 Show state:</b>",
|
|
136
|
+
parseMode: "html",
|
|
137
|
+
replyMarkup: { inline_keyboard: [
|
|
138
|
+
["global", "cwd"].map((scope) => ({ text: scope === "global" ? "Global" : "CWD", callback_data: callbackData("inspect", scope) })),
|
|
139
|
+
["session", "effective"].map((scope) => ({ text: scope === "session" ? "Session" : "Effective", callback_data: callbackData("inspect", scope) })),
|
|
140
|
+
] },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The complete message serializes each preformatted field one additional time;
|
|
145
|
+
// 3,000 leaves safe headroom for worst-case JSON escaping across all four fields.
|
|
146
|
+
const STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS = 3_000;
|
|
147
|
+
|
|
148
|
+
function renderStateFlowTelegramField(value: unknown): string {
|
|
149
|
+
const json = JSON.stringify(value, null, 2);
|
|
150
|
+
if (json.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) return json;
|
|
151
|
+
let low = 0;
|
|
152
|
+
let high = json.length;
|
|
153
|
+
let rendered = "";
|
|
154
|
+
while (low <= high) {
|
|
155
|
+
const length = Math.floor((low + high) / 2);
|
|
156
|
+
const candidate = JSON.stringify({
|
|
157
|
+
truncated: true,
|
|
158
|
+
preview: json.slice(0, length),
|
|
159
|
+
omittedChars: json.length - length,
|
|
160
|
+
}, null, 2);
|
|
161
|
+
if (candidate.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
162
|
+
rendered = candidate;
|
|
163
|
+
low = length + 1;
|
|
164
|
+
} else {
|
|
165
|
+
high = length - 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return rendered;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function renderStateFlowRichState(scope: StateFlowTelegramScope, state: StateFlowTelegramState): StateFlowTelegramRichMessage {
|
|
172
|
+
const title = scope === "cwd" ? "CWD" : `${scope[0].toUpperCase()}${scope.slice(1)}`;
|
|
173
|
+
const fields = ["artifacts", "contract", "working", "response"] as const;
|
|
174
|
+
return {
|
|
175
|
+
blocks: [{
|
|
176
|
+
type: "details",
|
|
177
|
+
summary: { type: "bold", text: title },
|
|
178
|
+
blocks: fields.map((field) => ({
|
|
179
|
+
type: "details",
|
|
180
|
+
summary: { type: "code", text: field },
|
|
181
|
+
blocks: [{ type: "pre", language: "json", text: renderStateFlowTelegramField(state[field]) }],
|
|
182
|
+
})),
|
|
183
|
+
}],
|
|
184
|
+
skip_entity_detection: true,
|
|
107
185
|
};
|
|
108
186
|
}
|
|
109
187
|
|
|
188
|
+
function isStateFlowTelegramScope(value: string): value is StateFlowTelegramScope {
|
|
189
|
+
return value === "global" || value === "cwd" || value === "session" || value === "effective";
|
|
190
|
+
}
|
|
191
|
+
|
|
110
192
|
function buildStateFlowTelegramSection(port: StateFlowTelegramPort) {
|
|
111
193
|
return {
|
|
112
194
|
id: STATE_FLOW_TELEGRAM_ID,
|
|
@@ -115,10 +197,21 @@ function buildStateFlowTelegramSection(port: StateFlowTelegramPort) {
|
|
|
115
197
|
render: (ctx: StateFlowTelegramSectionContext) =>
|
|
116
198
|
buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action)),
|
|
117
199
|
handleCallback: async (ctx: StateFlowTelegramCallbackContext) => {
|
|
118
|
-
// cancel/refresh remain routable for keyboards sent by earlier versions
|
|
119
|
-
if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh") return "pass" as const;
|
|
200
|
+
// cancel/refresh remain routable for keyboards sent by earlier versions.
|
|
201
|
+
if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh" && ctx.action !== "show-state" && ctx.action !== "inspect") return "pass" as const;
|
|
120
202
|
let notice: string | undefined;
|
|
121
203
|
try {
|
|
204
|
+
if (ctx.action === "show-state") {
|
|
205
|
+
await ctx.answerCallback();
|
|
206
|
+
await ctx.edit(buildStateFlowScopeChooser((action, payload) => ctx.callbackData(action, payload)));
|
|
207
|
+
return "handled" as const;
|
|
208
|
+
}
|
|
209
|
+
if (ctx.action === "inspect") {
|
|
210
|
+
if (!isStateFlowTelegramScope(ctx.payload)) throw new Error("Unknown State Flow scope");
|
|
211
|
+
await ctx.openRich(renderStateFlowRichState(ctx.payload, port.state(ctx.payload)));
|
|
212
|
+
await ctx.answerCallback();
|
|
213
|
+
return "handled" as const;
|
|
214
|
+
}
|
|
122
215
|
if (ctx.action === "start") {
|
|
123
216
|
if (port.canStartNow()) notice = port.start().message;
|
|
124
217
|
else {
|