@llblab/pi-kit 0.16.0 → 0.17.0
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 +6 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -1
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +16 -0
- package/node_modules/@llblab/pi-state-flow/README.md +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +31 -239
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +18 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +44 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +21 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +55 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +102 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +98 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +10 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +23 -129
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +22 -17
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +15 -6
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +33 -228
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +53 -1
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +18 -4
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +51 -5
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +96 -0
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +99 -11
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +45 -0
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +13 -2
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +79 -0
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +23 -129
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.17.0 - 2026-09-18
|
|
6
|
+
|
|
7
|
+
- `Proportional State Flow Guidance`: Advances the exact State Flow pin to `0.16.0`, adding the focused operational Skill, compacting memory curation guidance, and returning bounded reactive diagnostics for missing paths with durable references.
|
|
8
|
+
- `Intentional Agency`: Includes State Flow 0.15.0 hot scoped intents for selected future commitments, explicit references to supporting Lazy memory, complete lifecycle and migration semantics, and read-only Telegram inspection without introducing scheduling or automatic execution.
|
|
9
|
+
- `Package Cohort`: Keeps every other bundled package at its current exact version; the package set, resource inventory, and explicit load order remain unchanged.
|
|
10
|
+
|
|
5
11
|
## 0.16.0 - 2026-09-17
|
|
6
12
|
|
|
7
13
|
- `Canonical Knowledge Memory`: Advances the exact State Flow pin to `0.14.0`, making Global Lazy the sole canonical Knowledge store and removing the former Markdown Knowledge layer while preserving scoped semantic compilation and recovery.
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
14
14
|
| [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.1.1` | Isolated nested Pi TUI with explicitly selected extensions |
|
|
15
15
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.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
|
+
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.16.0` | Incremental scoped state/context/memory compiler with proportional operational guidance, intentional agency, reactive dangling-reference diagnostics, compiled runtime delivery, safe compaction, and exact publication |
|
|
18
18
|
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.49.0` | Telegram companion with native fresh-session replacement, adaptive Thread continuity, exact queues, files, voice, controls, and Generative Apps guidance |
|
|
19
19
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
20
20
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Agent Instructions
|
|
2
2
|
|
|
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
|
|
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, except State Flow's own validation feedback when it is represented separately.
|
|
6
|
-
- Expose one canonical materialized state shape across global, CWD, and session scopes with exactly `artifacts`, `contract`, `working`, and `response`; the first
|
|
6
|
+
- Expose one canonical materialized state shape across global, CWD, and session scopes with exactly `artifacts`, `contract`, `working`, `intents`, and `response`, plus optional `lazy`; the first four are flexible semantic objects and `response` is the latest complete user-facing answer string where applicable. `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 freshness evidence (`sourceHash`, `compilerRevision`, `compiledAt`) belongs in the scope `meta.json` provenance registry, never in projected semantic 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
|
- After local activation and before the next enabled inference, discover regular lowercase `*.md` files recursively beneath the configured Knowledge repository root; use canonical absolute paths, hash opaque source bytes and retain their byte counts without decoding or retaining bodies, skip symlinks, and never traverse outside that canonical root. Treat the current candidate set plus explicit owner-confirmed removals as generic global artifact input without invoking Knowledge validators. Partial candidate sets, unavailable roots, and symlink/external/non-Markdown paths never authorize deletion. Re-derive removals from the retained global registry so status inspection and restart cannot consume uncommitted observations; unavailable roots mean unknown freshness. Materialize removals deterministically as runtime-owned global transitions without rereading missing bodies or requesting model compiler output. Never reserve Knowledge document names, provide built-in Knowledge templates, or implement `save_knowledge`.
|
|
9
9
|
- Derive artifact freshness per available evidence: a new source always requires compilation, a matching `sourceHash` or `compilerRevision` detects its own change, malformed present evidence fails closed for the capability that depends on it, and missing evidence degrades to an unknown-but-usable artifact rather than corrupt state or a migration. Never treat `compiled_at` as a correctness signal. Plan acquisition from path/hash identity without source bodies, project stale global candidates as runtime-owned `artifact_invalidations` containing only `{path, reason}`, and publish accepted compilation semantics plus matching provenance in one durable cohort.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- Keep historical reads lazy through the smallest runtime/model read interface. Normal inference gets only current effective state and useful bounded compact transition context, never eight full snapshots. `patch_state` is the sole semantic mutation tool. `read_state` treats unscoped semantic paths as current effective aliases, accepts explicit effective/scoped materializations and scope-local retained accepted patches, and rejects the retired top-level `state` segment; materialization indices share the composed causal lineage while `patches[n]` walks the selected scope's retained patch tail. The public reader accepts only `path` or `paths`; temporal offsets and scope selection are expressed in path syntax. Reads return the exact boundary and never publish, append a checkpoint, or advance history. Both tools follow branch enablement and host tool restrictions; the patch barrier blocks reader siblings too. Live/cached checkpoint plus tails own the hot path; use Git for branch restoration and explicit cold inspection, not to rebuild current state on every inference.
|
|
20
20
|
- Store runtime state in its own directory with optional Git durability, defaulting to `state-flow/` beneath Pi's configured agent directory (`~/.pi/agent/state-flow/` normally), independently from the Knowledge Markdown source root. Use exactly these owned paths: `checkpoint.json`, `patches.jsonl`, and `meta.json` for global; `<cwd-key>/checkpoint.json`, `<cwd-key>/patches.jsonl`, and `<cwd-key>/meta.json` for CWD; `<cwd-key>/<session-key>/checkpoint.json`, `patches.jsonl`, `meta.json`, `config.json`, and `runtime.json` for session. Do not create `.state-flow`, `scopes`, or another storage/history namespace. Mirror Pi's native CWD session-directory encoding and JSONL-basename session key (deriving `<header timestamp>_<UUID>` for in-memory sessions), while retaining separately verifiable canonical identity provenance in owned state. Keep the UUID authoritative, reject unsafe segments and mismatches rather than selecting another scope, and use CWD `meta.json` ownership to fail closed on Pi-name collisions.
|
|
21
21
|
- Classify every filesystem cohort before recovery: complete valid evidence is usable; total absence is recoverable only from an explicit semantic default or exact surviving authority; partial, malformed, contradictory, or authority-losing evidence fails closed at the smallest dependent capability. A wholly absent untouched global/CWD checkpoint-tail pair is current empty shared reality and must not resurrect selected cold values; a patch targeting that disappeared scope is stale and must fail for reinference. Missing provenance means unavailable freshness, not semantic corruption. Perform every repair through normal locked/CAS publication, never ad-hoc writes.
|
|
22
|
-
- Support the release-owned storage migration chain through the migration domain: complete predecessor checkpoint/tail envelopes convert into semantic-only checkpoint/tail files plus temporal `meta.json`,
|
|
22
|
+
- Support the release-owned storage migration chain through the migration domain: complete predecessor checkpoint/tail envelopes convert into semantic-only checkpoint/tail files plus temporal `meta.json`, the 0.13 → 0.14 step splits every owner-proven session's combined `meta.json` into scope-only `meta.json` plus branch/run `runtime.json`; and the 0.14 → 0.15 step adds empty `intents` to retained semantic checkpoints without inferring commitments. Execute the applicable steps through one normal locked/CAS publication. Replace a Git-backed semantic-envelope store's history with one new root commit for the complete migrated tree; apply the same cohort without Git in file-only mode. `state.json`, pre-0.4 hashed layouts, and semantic Pi checkpoint envelopes are unsupported and never become recovery authority. Exercise migrations in temporary repositories rather than modifying the user's active data during development.
|
|
23
23
|
- Read and write only regular non-symlink State Flow-owned files at those exact paths, publish each file by same-directory atomic rename, preserve arbitrary repository contents, and classify ownership exactly. Git acceptance follows the complete-delta isolated-index contract below. Retain opaque source bytes for file identity and rollback, not decoded-text reconstructions. Use validated structural JSON equality for in-process semantic comparisons; reserve cryptographic hashes for compact identities that cross inference, persistence, process, or source-freshness boundaries. Serialize cooperating Git publications through a common-Git-directory publication lock; low-level file helpers require caller exclusion. Recheck prepared bases before each replacement/deletion and restore only bytes still matching the publisher's own output. Preserve detected concurrent changes and report unresolved rollback conflicts; do not claim kernel-atomic multi-file CAS against nonparticipating writers. Markdown discovery uses its independently configured source root, defaulting to `knowledge/` beneath Pi's agent directory; a storage-root override must not redirect source discovery.
|
|
24
24
|
- At instance initialization resolve the repository, CWD key, and session key; load each scope's anchored checkpoint and tail, materialize at the selected temporal boundary, then overlay `global → cwd → session`. Install cached view and publication basis atomically only after successful restoration/initialization; unavailable publication is an error, not a semantic no-op. Branch recovery may reuse one owner-bound, single-use inspection of the exact selected immutable Git revision, with a detached inspection snapshot and a freshly acquired live publication basis at installation. Revalidate mutable file cohorts, legacy snapshot fallbacks, and redirected runtime owners; never turn inspection into a long-lived revision or publication-basis cache. Preserve the selected revision across transient restore failure so an explicit start can retry it. A genuinely new session gets an empty session layer and inherits only global/CWD values; it must never reuse another same-CWD session layer. Explicit native fork adoption instead follows the [session-copy contract](docs/fork-contract.md): copy the proven source session checkpoint/tail and provenance into a distinct owner over current shared streams, without pruning shared provenance or modifying the parent. Use a fresh origin, preserve copied replay records, reject occupied live/HEAD targets, and publish/checkpoint the child only after CAS acceptance. Inherited parent pointers never authorize an empty reset; fence parent passive Stop projection across child reload.
|
|
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.
|
|
@@ -29,7 +29,7 @@
|
|
|
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
|
-
- 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.
|
|
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. 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.
|
|
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
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.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
# BACKLOG
|
|
2
2
|
|
|
3
|
-
Completed
|
|
3
|
+
Completed implementation belongs in [CHANGELOG.md](CHANGELOG.md); durable semantic contracts belong in [AGENTS.md](AGENTS.md) and [docs/architecture.md](docs/architecture.md).
|
|
4
|
+
|
|
5
|
+
No open work.
|
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.16.0: Proportional State Flow guidance
|
|
8
|
+
|
|
9
|
+
- `Operational Skill`: Adds the packaged `state-flow-guide` as an on-demand reference for concrete read, patch, inheritance, acquisition, finalization, and recovery problems without triggering memory audits or unsolicited cleanup.
|
|
10
|
+
- `Memory Skill`: Compresses `state-flow-memory` into one bounded curation procedure while preserving authority separation, intent cleanup, evidence boundaries, ownership checks, two-phase scope transfer, external acceptance verification, and active/passive finalization behavior.
|
|
11
|
+
- `Contract alignment`: Updates executable Skill discovery and release-package inventory checks for both Skills, removes obsolete documentation for retired read syntax, and defines state references as structured `$ref` values or `$`-prefixed `read_state` paths in prose without proactive link scanning.
|
|
12
|
+
- `Reference diagnostics`: When one requested value path is missing and exact durable sources exist, returns `{value:null, hint:[...]}` with a typed reconciliation message and at most three runtime-verified current owning paths. Keys, patch, batch, and unmatched reads remain all-or-error; no match never implies that the agent invented the path.
|
|
13
|
+
- `Composition root`: Reduces `lib/extension.ts` to higher-level Pi lifecycle wiring by moving patch presentation into `protocol`, branch traversal into `session`, diagnostic persistence into `logging`, and durable queue/worker lifecycle into `publication`, without adding domains or changing public behavior.
|
|
14
|
+
|
|
15
|
+
## 0.15.0: Intentional agency
|
|
16
|
+
|
|
17
|
+
- `Intent semantic plane`: Adds hot, object-valued `intents` to global, CWD, session, and effective state. An intent is a selected commitment to future action, distinct from requirements in `contract`, observations or possibilities in `working`, and inactive supporting memory in `lazy`.
|
|
18
|
+
- `Intent lifecycle`: Supports ordinary atomic creation, update, scope overlay, supersession, and removal. Active intents survive intermediate handoffs; fulfilled, abandoned, superseded, or impossible intents disappear while consequential results and referenced state remain independently retained.
|
|
19
|
+
- `Explicit semantic references`: Intents may carry conventional `{"$ref":"cwd.lazy.plan"}` pointers. Reads return references exactly and follow their targets only through a separate explicit `read_state`; State Flow adds no dependency graph, automatic hydration, scheduling, execution, or completion behavior.
|
|
20
|
+
- `Durability and migration`: Preserves intents across hot history, Git and file-only persistence, passive and active operation, forks, compaction, and CAS conflicts. The explicit 0.14 → 0.15 migration adds empty intents to retained scopes without inferring commitments from working state, plans, requirements, lazy memory, or response prose.
|
|
21
|
+
- `Inspection and guidance`: Extends current, scoped, historical, key, patch, and batch reads; adds read-only Telegram intent inspection; and updates the model protocol and bundled memory Skill to reconcile active commitments without introducing a task manager or second agent loop.
|
|
22
|
+
|
|
7
23
|
## 0.14.0: Passive and progressive memory
|
|
8
24
|
|
|
9
25
|
- `Default passive memory`: Repository-root configuration now adds independently configurable passive bootstrap and `read_state`/`patch_state` access, both enabled by default. Passive reads project durable memory without mutation; explicit patches may materialize storage while episode barriers, continuation, response reconciliation, and compaction remain inactive. Start promotes to active semantics and Stop returns to passive mode.
|
|
@@ -58,17 +58,20 @@ Active State Flow episodes remain **opt-in**, while passive durable memory boots
|
|
|
58
58
|
The same semantic planes exist at every scope:
|
|
59
59
|
|
|
60
60
|
- `contract`: Requirements, decisions, constraints, and interface commitments.
|
|
61
|
-
- `working`: Observations, results, unresolved questions, and
|
|
61
|
+
- `working`: Observations, results, unresolved questions, and possible next steps.
|
|
62
|
+
- `intents`: Courses of action the agent has actually committed to pursue.
|
|
62
63
|
- `artifacts`: Source-addressed descriptions and reusable compiled knowledge.
|
|
63
64
|
- `response`: The latest complete answer, captured by the runtime.
|
|
64
65
|
- `lazy`: Durable, versioned memory omitted from ordinary context until explicitly read.
|
|
65
66
|
|
|
66
|
-
Memory overlays **global → project CWD → session**. Put reusable cross-project knowledge in global, project knowledge in CWD, and private task continuation in session. Hot planes carry what must matter now; `lazy` retains what may matter later without hydrating its body into every prompt.
|
|
67
|
+
Memory overlays **global → project CWD → session**. Put reusable cross-project knowledge in global, project knowledge in CWD, and private task continuation in session. Hot planes carry what must matter now; `lazy` retains what may matter later without hydrating its body into every prompt. An intent stays hot only while its course remains chosen and disappears when fulfilled, abandoned, superseded, or impossible. It may refer to supporting detail through a structured `{"$ref":"cwd.lazy.plan"}` value or a `$`-prefixed state path inside ordinary prose, such as `$effective.lazy.memory[7]`. Both remain semantic content interpreted by the agent: State Flow never parses, validates, hydrates, executes, or completes them automatically. The agent never scans references for breakage. Only when current work already follows one and a single value path is missing does State Flow perform a bounded exact reverse lookup. Matching durable sources produce a top-level `{value:null, hint:[...]}` sentinel whose typed hint names runtime-verified current owning paths and asks the agent to reconcile them. Keys, patch, batch, and unmatched reads retain ordinary all-or-error behavior; no match does not prove that the agent invented the path. The agent may then repair or remove a proven stale locator in its owning value.
|
|
67
68
|
|
|
68
69
|
**Resuming an existing Pi session restores its selected State Flow state and enablement.** A genuinely new session starts with an empty session layer and inherits only shared global/CWD memory; it does not resume another session's private work. Tree navigation follows the selected branch, not whichever state happens to be at Git `HEAD`.
|
|
69
70
|
|
|
70
71
|
The agent uses `read_state` for exact current or historical paths. Unscoped paths read the effective overlay; `global`, `cwd`, and `session` address ownership directly; `[n]` selects one of up to seven prior accepted transition boundaries. Bounded array ranges and `value`, `keys`, or `patch` projections support progressive reads without hydrating whole lazy collections. Git-backed stores retain older committed history separately.
|
|
71
72
|
|
|
73
|
+
Two packaged Skills keep guidance proportional: `state-flow-guide` resolves concrete operational questions about reading, patching, inheritance, acquisition, finalization, and recovery; `state-flow-memory` performs one bounded explicit or phase-boundary curation. Neither runs background maintenance.
|
|
74
|
+
|
|
72
75
|
## Boundaries worth knowing
|
|
73
76
|
|
|
74
77
|
- `Not a second agent loop`: State Flow adds memory to Pi; it does not run background reasoning or replace session controls.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
2
2
|
import { type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type StateScope } from "./state.ts";
|
|
4
4
|
/** Canonical semantic sources plus runtime-owned temporal metadata. */
|
|
5
5
|
export interface ScopeStreamSources {
|
|
6
6
|
checkpoint: string;
|
|
@@ -5,6 +5,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import { parseArtifactProvenanceRegistry, serializeArtifactProvenanceRegistry, } from "./artifact.js";
|
|
6
6
|
import { canonicalJson, isJsonValue, isObject } from "./json.js";
|
|
7
7
|
import { validateScopeStream, validateTemporalState } from "./temporal.js";
|
|
8
|
+
import { migratePreIntentState } from "./state.js";
|
|
8
9
|
const SESSION_KEY_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
9
10
|
const STATE_FILE = "state.json";
|
|
10
11
|
const CHECKPOINT_FILE = "checkpoint.json";
|
|
@@ -65,6 +66,7 @@ export function classifyScopeStream(checkpointSource, patchesSource, scope, expe
|
|
|
65
66
|
}
|
|
66
67
|
const temporal = meta?.temporal;
|
|
67
68
|
if (isObject(temporal) && Object.hasOwn(temporal, "checkpoint") && Array.isArray(temporal.patches)) {
|
|
69
|
+
checkpoint = migratePreIntentState(checkpoint) ?? checkpoint;
|
|
68
70
|
const owner = meta?.owner;
|
|
69
71
|
if (scope === "cwd" && expectedCwd !== undefined
|
|
70
72
|
&& (!isObject(owner) || Object.keys(owner).join(",") !== "cwd" || owner.cwd !== resolve(expectedCwd))) {
|
|
@@ -94,6 +96,11 @@ export function classifyScopeStream(checkpointSource, patchesSource, scope, expe
|
|
|
94
96
|
else if (scope === "cwd" && expectedCwd !== undefined) {
|
|
95
97
|
throw new Error("State Flow CWD scope identity is missing");
|
|
96
98
|
}
|
|
99
|
+
if (isObject(legacyCheckpoint) && isObject(legacyCheckpoint.state)) {
|
|
100
|
+
const migrated = migratePreIntentState(legacyCheckpoint.state);
|
|
101
|
+
if (migrated)
|
|
102
|
+
legacyCheckpoint = { ...legacyCheckpoint, state: migrated };
|
|
103
|
+
}
|
|
97
104
|
const stream = { checkpoint: legacyCheckpoint, patches };
|
|
98
105
|
validateScopeStream(stream, scope);
|
|
99
106
|
return { kind: "present", stream };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatPatchStateArguments, normalizePatchStateArguments } from "./protocol.ts";
|
|
2
3
|
import { type StateFlowTelegramLoader } from "./telegram.ts";
|
|
3
4
|
import { type MaterializedState, type StateScope } from "./state.ts";
|
|
4
5
|
export interface StateFlowExtensionOptions {
|
|
@@ -17,11 +18,8 @@ export interface StateFlowExtensionOptions {
|
|
|
17
18
|
tools?: boolean;
|
|
18
19
|
};
|
|
19
20
|
}
|
|
21
|
+
export { formatPatchStateArguments, normalizePatchStateArguments };
|
|
20
22
|
export declare const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
21
23
|
export declare const READ_STATE_TOOL_NAME = "read_state";
|
|
22
24
|
export declare const MAX_FALLBACK_ATTEMPTS: number;
|
|
23
|
-
/** Keep successful patch JSON valid while separating adjacent scopes and memory sections visually. */
|
|
24
|
-
export declare function formatPatchStateArguments(args: unknown): string;
|
|
25
|
-
/** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
|
|
26
|
-
export declare function normalizePatchStateArguments(args: unknown): any;
|
|
27
25
|
export default function stateFlowExtension(pi: ExtensionAPI, options?: StateFlowExtensionOptions): void;
|
|
@@ -2,97 +2,38 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
-
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.js";
|
|
5
|
+
import { assistantToolCallCount, finalizedAssistantResponse, formatPatchStateArguments, normalizePatchStateArguments, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.js";
|
|
6
6
|
import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, passiveContinuationMessages, runtimeContextMessage, syntheticUser, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.js";
|
|
7
7
|
import { ArtifactReadTracker } from "./acquisition.js";
|
|
8
8
|
import { loadStateFlowConfig } from "./config.js";
|
|
9
9
|
import { createStateFlowTelegramAdapter } from "./telegram.js";
|
|
10
|
-
import { isAbsolute,
|
|
10
|
+
import { isAbsolute, resolve } from "node:path";
|
|
11
11
|
import { SkillReadTracker } from "./skills.js";
|
|
12
12
|
import { emptySnapshot, migrationFailure, persistableSnapshot, RevisionUnavailableError } from "./snapshot.js";
|
|
13
13
|
import { readNativeSessionHeader } from "./continuation.js";
|
|
14
14
|
import { MissingSessionRuntimeError, SharedScopeRemovalConflictError, TemporalRuntime } from "./runtime.js";
|
|
15
15
|
import { emptyState, overlayStates, projectStateForModel } from "./state.js";
|
|
16
16
|
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility } from "./transition.js";
|
|
17
|
-
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.js";
|
|
17
|
+
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
|
|
18
18
|
import { compactStatus, detailedStatus, STATUS_KEY } from "./status.js";
|
|
19
19
|
import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
|
|
20
20
|
import { recoverSnapshot } from "./recovery.js";
|
|
21
|
-
import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.js";
|
|
22
|
-
import { coalescePublicationTarget, createPublicationQueue } from "./publication.js";
|
|
23
|
-
import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.js";
|
|
24
|
-
import { runPublicationWorker } from "./publication.js";
|
|
21
|
+
import { loadPublicationQueue, publicationQueuePath, PublicationWorkerController, resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.js";
|
|
25
22
|
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.js";
|
|
26
23
|
import { canonicalJson, isObject, sameJson } from "./json.js";
|
|
27
24
|
import { readProjectedState, readStatePath } from "./query.js";
|
|
28
25
|
import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
|
|
29
26
|
import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.js";
|
|
30
|
-
import {
|
|
27
|
+
import { StateFlowDiagnosticWriter, stateFlowLogPath } from "./logging.js";
|
|
31
28
|
import { isGitCommitAncestor, pushGitCommit, pushGitTarget, resolveGitPushDestination } from "./git.js";
|
|
32
29
|
import { ORDINARY_ARTIFACT_COMPILER, planArtifactInvalidation, } from "./artifact.js";
|
|
33
30
|
import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult } from "./compaction.js";
|
|
31
|
+
export { formatPatchStateArguments, normalizePatchStateArguments };
|
|
34
32
|
export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
35
33
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
36
34
|
export const MAX_FALLBACK_ATTEMPTS = 2;
|
|
37
35
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
38
36
|
const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
|
|
39
|
-
const PATCH_DISPLAY_SECTION_KEYS = new Set([
|
|
40
|
-
"global",
|
|
41
|
-
"cwd",
|
|
42
|
-
"session",
|
|
43
|
-
"artifacts",
|
|
44
|
-
"contract",
|
|
45
|
-
"working",
|
|
46
|
-
"response",
|
|
47
|
-
"final",
|
|
48
|
-
]);
|
|
49
|
-
/** Keep successful patch JSON valid while separating adjacent scopes and memory sections visually. */
|
|
50
|
-
export function formatPatchStateArguments(args) {
|
|
51
|
-
const seenAtIndent = new Set();
|
|
52
|
-
return JSON.stringify(args, null, 2).split("\n").flatMap((line) => {
|
|
53
|
-
const indent = line.length - line.trimStart().length;
|
|
54
|
-
const match = /^(\s+)"([^"]+)":/.exec(line);
|
|
55
|
-
if (match === null || !PATCH_DISPLAY_SECTION_KEYS.has(match[2])) {
|
|
56
|
-
for (const seenIndent of seenAtIndent) {
|
|
57
|
-
if (seenIndent > indent)
|
|
58
|
-
seenAtIndent.delete(seenIndent);
|
|
59
|
-
}
|
|
60
|
-
return [line];
|
|
61
|
-
}
|
|
62
|
-
const separator = seenAtIndent.has(indent) ? [""] : [];
|
|
63
|
-
seenAtIndent.add(indent);
|
|
64
|
-
return [...separator, line];
|
|
65
|
-
}).join("\n");
|
|
66
|
-
}
|
|
67
|
-
/** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
|
|
68
|
-
export function normalizePatchStateArguments(args) {
|
|
69
|
-
if (!isObject(args) || !Object.hasOwn(args, "final"))
|
|
70
|
-
return args;
|
|
71
|
-
const value = args.final;
|
|
72
|
-
let final;
|
|
73
|
-
if (typeof value === "boolean")
|
|
74
|
-
final = value;
|
|
75
|
-
else if (value === 1)
|
|
76
|
-
final = true;
|
|
77
|
-
else if (value === 0)
|
|
78
|
-
final = false;
|
|
79
|
-
else if (typeof value === "string" && value.trim().toLowerCase() === "true")
|
|
80
|
-
final = true;
|
|
81
|
-
else if (typeof value === "string" && value.trim().toLowerCase() === "false")
|
|
82
|
-
final = false;
|
|
83
|
-
else
|
|
84
|
-
return args;
|
|
85
|
-
return { ...args, final };
|
|
86
|
-
}
|
|
87
|
-
/** Keep tool output visually separated from its heading with exactly one leading newline. */
|
|
88
|
-
function separatedOutput(text) {
|
|
89
|
-
return `\n${text.replace(/^\n+/, "")}`;
|
|
90
|
-
}
|
|
91
|
-
/** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
|
|
92
|
-
function separatedFailure(error) {
|
|
93
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
-
return new Error(separatedOutput(message), error instanceof Error ? { cause: error } : undefined);
|
|
95
|
-
}
|
|
96
37
|
export default function stateFlowExtension(pi, options = {}) {
|
|
97
38
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
98
39
|
const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
|
|
@@ -125,15 +66,19 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
125
66
|
let pendingPublication;
|
|
126
67
|
let rehydrationPhase;
|
|
127
68
|
let turnPublicationTarget;
|
|
128
|
-
const activePublicationWorkers = new Map();
|
|
129
|
-
let publicationStopped = false;
|
|
130
69
|
let publicationShutdown;
|
|
131
70
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
71
|
+
const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => activeContext?.ui.notify(message, "warning"));
|
|
72
|
+
const publicationWorker = new PublicationWorkerController({
|
|
73
|
+
resolveDestination: () => resolveGitPushDestination(repositoryRoot),
|
|
74
|
+
isAncestor: (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
|
|
75
|
+
push: (destination, target, signal) => pushGitTarget(repositoryRoot, destination, target, signal),
|
|
76
|
+
onDiverged: (dropped, target) => activeContext && recordDiagnostic(`Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`, "publication-conflict", activeContext),
|
|
77
|
+
});
|
|
132
78
|
const skillReads = new SkillReadTracker();
|
|
133
79
|
const artifactReads = new ArtifactReadTracker();
|
|
134
80
|
const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(agentDir));
|
|
135
81
|
let artifactInvalidations = [];
|
|
136
|
-
let loggingWarningReported = false;
|
|
137
82
|
let telegramStartPending = false;
|
|
138
83
|
function sessionAddress(ctx) {
|
|
139
84
|
return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
|
|
@@ -184,29 +129,6 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
184
129
|
skillReads.clear();
|
|
185
130
|
artifactReads.clear();
|
|
186
131
|
}
|
|
187
|
-
function passiveStopBoundary(ctx) {
|
|
188
|
-
for (const entry of [...ctx.sessionManager.getBranch()].reverse()) {
|
|
189
|
-
try {
|
|
190
|
-
if (entry?.type !== "custom" || entry.customType !== PASSIVE_STOP_ENTRY_TYPE)
|
|
191
|
-
continue;
|
|
192
|
-
const { at, from, reset, owner } = entry.data ?? {};
|
|
193
|
-
if (reset === true && owner === ctx.sessionManager.getSessionId())
|
|
194
|
-
return undefined;
|
|
195
|
-
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0)
|
|
196
|
-
return {
|
|
197
|
-
at,
|
|
198
|
-
...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
function retainsPhysicalSessionProjection(reason) {
|
|
208
|
-
return reason === undefined || reason === "startup" || reason === "reload" || reason === "resume";
|
|
209
|
-
}
|
|
210
132
|
function deferArtifactRefresh() {
|
|
211
133
|
artifactInvalidations = [];
|
|
212
134
|
artifactReads.setCandidates([]);
|
|
@@ -252,22 +174,6 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
252
174
|
? [...new Set([...active, ...owned])]
|
|
253
175
|
: active.filter((name) => !owned.includes(name)));
|
|
254
176
|
}
|
|
255
|
-
function assistantToolBatch(ctx, toolCallId) {
|
|
256
|
-
for (let cursor = ctx.sessionManager.getLeafEntry(); cursor; cursor = cursor.parentId ? ctx.sessionManager.getEntry(cursor.parentId) : undefined) {
|
|
257
|
-
const entry = cursor;
|
|
258
|
-
if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content))
|
|
259
|
-
continue;
|
|
260
|
-
const calls = entry.message.content.filter((block) => {
|
|
261
|
-
return typeof block === "object" && block !== null
|
|
262
|
-
&& block.type === "toolCall"
|
|
263
|
-
&& typeof block.id === "string"
|
|
264
|
-
&& typeof block.name === "string";
|
|
265
|
-
});
|
|
266
|
-
if (calls.some(({ id }) => id === toolCallId))
|
|
267
|
-
return calls.map(({ name }) => name);
|
|
268
|
-
}
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
177
|
function recordPublication(publication, ctx) {
|
|
272
178
|
const revision = publication.revision ?? publication.commit;
|
|
273
179
|
if (revision !== undefined)
|
|
@@ -294,8 +200,8 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
294
200
|
return;
|
|
295
201
|
turnPublicationTarget = target;
|
|
296
202
|
try {
|
|
297
|
-
enqueueTurnPublication(
|
|
298
|
-
|
|
203
|
+
enqueueTurnPublication();
|
|
204
|
+
publicationWorker.launch();
|
|
299
205
|
}
|
|
300
206
|
catch (error) {
|
|
301
207
|
ctx.ui.notify(`State Flow accepted the local commit; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
@@ -338,104 +244,17 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
338
244
|
persist();
|
|
339
245
|
return true;
|
|
340
246
|
}
|
|
341
|
-
function enqueueTurnPublication(
|
|
247
|
+
function enqueueTurnPublication() {
|
|
342
248
|
const target = turnPublicationTarget;
|
|
343
249
|
turnPublicationTarget = undefined;
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
const destination = resolveGitPushDestination(repositoryRoot);
|
|
347
|
-
if (!destination)
|
|
348
|
-
return;
|
|
349
|
-
const path = publicationQueuePath(destination);
|
|
350
|
-
const previous = loadPublicationQueue(path);
|
|
351
|
-
const next = previous
|
|
352
|
-
? coalescePublicationTarget(previous, destination, target, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant), {
|
|
353
|
-
onDivergedLineage: (dropped) => recordDiagnostic(`Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`, "publication-conflict", ctx),
|
|
354
|
-
})
|
|
355
|
-
: createPublicationQueue(destination, target);
|
|
356
|
-
savePublicationQueue(path, next, previous);
|
|
357
|
-
}
|
|
358
|
-
function launchPublicationWorker() {
|
|
359
|
-
if (publicationStopped)
|
|
360
|
-
return;
|
|
361
|
-
let destination;
|
|
362
|
-
try {
|
|
363
|
-
destination = resolveGitPushDestination(repositoryRoot);
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
if (error instanceof Error && /ENOENT/.test(error.message))
|
|
367
|
-
return;
|
|
368
|
-
throw error;
|
|
369
|
-
}
|
|
370
|
-
if (!destination)
|
|
371
|
-
return;
|
|
372
|
-
const path = publicationQueuePath(destination);
|
|
373
|
-
if (activePublicationWorkers.has(path))
|
|
374
|
-
return;
|
|
375
|
-
let queued;
|
|
376
|
-
let lease;
|
|
377
|
-
try {
|
|
378
|
-
queued = loadPublicationQueue(path);
|
|
379
|
-
if (!queued)
|
|
380
|
-
return;
|
|
381
|
-
lease = acquirePublicationWorkerLease(path);
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (!lease)
|
|
387
|
-
return;
|
|
388
|
-
const controller = new AbortController();
|
|
389
|
-
const done = runPublicationWorker(queued, ({ target }) => pushGitTarget(repositoryRoot, destination, target, controller.signal), () => loadPublicationQueue(path) ?? queued, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant)).then((result) => {
|
|
390
|
-
if (publicationStopped)
|
|
391
|
-
return; // Late outcomes belong to an unconfirmed queue, not the replacement generation.
|
|
392
|
-
const current = loadPublicationQueue(path);
|
|
393
|
-
if (!current)
|
|
394
|
-
return;
|
|
395
|
-
if (result.next === undefined) {
|
|
396
|
-
if (current.target === result.attempted.target)
|
|
397
|
-
removePublicationQueue(path, current);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (current.target === result.attempted.target || result.next.target === current.target) {
|
|
401
|
-
savePublicationQueue(path, result.next, current);
|
|
402
|
-
}
|
|
403
|
-
}).catch(() => {
|
|
404
|
-
// Queue/CAS truth remains durable; status and a later activation expose retry.
|
|
405
|
-
}).finally(() => {
|
|
406
|
-
activePublicationWorkers.delete(path);
|
|
407
|
-
try {
|
|
408
|
-
lease.release();
|
|
409
|
-
if (!publicationStopped && loadPublicationQueue(path)?.status === "pending")
|
|
410
|
-
launchPublicationWorker();
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
// Failed lease cleanup or malformed persistence stays inert until retry or repair.
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
activePublicationWorkers.set(path, { controller, done });
|
|
250
|
+
if (target)
|
|
251
|
+
publicationWorker.enqueue(target);
|
|
417
252
|
}
|
|
418
253
|
function shutdownPublicationWorkers(ctx) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
controller.abort();
|
|
424
|
-
if (workers.length === 0)
|
|
425
|
-
return;
|
|
426
|
-
let timeout;
|
|
427
|
-
try {
|
|
428
|
-
const completed = await Promise.race([
|
|
429
|
-
Promise.all(workers.map(({ done }) => done)).then(() => true),
|
|
430
|
-
new Promise((resolve) => { timeout = setTimeout(() => resolve(false), PUBLICATION_SHUTDOWN_WAIT_MS); }),
|
|
431
|
-
]);
|
|
432
|
-
if (!completed)
|
|
433
|
-
ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
|
|
434
|
-
}
|
|
435
|
-
finally {
|
|
436
|
-
clearTimeout(timeout);
|
|
437
|
-
}
|
|
438
|
-
})();
|
|
254
|
+
return publicationShutdown ??= publicationWorker.shutdown(PUBLICATION_SHUTDOWN_WAIT_MS).then((completed) => {
|
|
255
|
+
if (!completed)
|
|
256
|
+
ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
|
|
257
|
+
});
|
|
439
258
|
}
|
|
440
259
|
function retryPendingPush(ctx) {
|
|
441
260
|
if (pendingPublication === undefined || !runtime?.view)
|
|
@@ -447,8 +266,8 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
447
266
|
if (mode === "turn-end") {
|
|
448
267
|
turnPublicationTarget = target;
|
|
449
268
|
try {
|
|
450
|
-
enqueueTurnPublication(
|
|
451
|
-
|
|
269
|
+
enqueueTurnPublication();
|
|
270
|
+
publicationWorker.launch();
|
|
452
271
|
}
|
|
453
272
|
catch (error) {
|
|
454
273
|
ctx.ui.notify(`State Flow retained local state; asynchronous publication recovery is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
@@ -655,7 +474,7 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
655
474
|
ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
|
|
656
475
|
}
|
|
657
476
|
if (retainsPhysicalSessionProjection(sessionStartReason) && runtime?.view) {
|
|
658
|
-
const boundary =
|
|
477
|
+
const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
659
478
|
if (boundary !== undefined) {
|
|
660
479
|
const continuation = createPassiveContinuation(projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)), boundary.at, boundary.from);
|
|
661
480
|
if (!snapshot.config.enabled)
|
|
@@ -679,34 +498,7 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
679
498
|
updateUi(ctx);
|
|
680
499
|
}
|
|
681
500
|
function recordDiagnostic(error, category, ctx, extras = {}) {
|
|
682
|
-
|
|
683
|
-
return;
|
|
684
|
-
try {
|
|
685
|
-
const path = stateFlowLogPath(agentDir);
|
|
686
|
-
const fromRepository = relative(repositoryRoot, path);
|
|
687
|
-
if (fromRepository === "" || (!isAbsolute(fromRepository) && fromRepository !== ".." && !fromRepository.startsWith(`..${sep}`))) {
|
|
688
|
-
throw new Error("diagnostic path overlaps the State Flow repository");
|
|
689
|
-
}
|
|
690
|
-
appendStateFlowDiagnostic(path, {
|
|
691
|
-
at: new Date().toISOString(),
|
|
692
|
-
sessionId: sessionAddress(ctx).id,
|
|
693
|
-
cwd: resolve(ctx.cwd),
|
|
694
|
-
category,
|
|
695
|
-
error,
|
|
696
|
-
...(extras.content === undefined ? {} : { content: projectDiagnosticContent(extras.content) }),
|
|
697
|
-
...(extras.input === undefined ? {} : { input: extras.input }),
|
|
698
|
-
...(extras.tool === undefined ? {} : { tool: extras.tool }),
|
|
699
|
-
...(extras.toolCallId === undefined ? {} : { toolCallId: extras.toolCallId }),
|
|
700
|
-
...(extras.resolutionAttempt === undefined ? {} : { resolutionAttempt: extras.resolutionAttempt }),
|
|
701
|
-
...(extras.terminalEligible === undefined ? {} : { terminalEligible: extras.terminalEligible }),
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
catch (failure) {
|
|
705
|
-
if (loggingWarningReported)
|
|
706
|
-
return;
|
|
707
|
-
loggingWarningReported = true;
|
|
708
|
-
ctx.ui.notify(`State Flow could not write diagnostics: ${failure instanceof Error ? failure.message : String(failure)}`, "warning");
|
|
709
|
-
}
|
|
501
|
+
diagnosticWriter.record(sessionAddress(ctx).id, ctx.cwd, error, category, extras);
|
|
710
502
|
}
|
|
711
503
|
function continueFallbackResolution(lastWarning) {
|
|
712
504
|
resolutionPending = true;
|
|
@@ -1171,7 +963,7 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1171
963
|
pi.on("tool_call", (event, ctx) => {
|
|
1172
964
|
if (!snapshot.config.enabled)
|
|
1173
965
|
return;
|
|
1174
|
-
const batch =
|
|
966
|
+
const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
|
|
1175
967
|
const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
|
|
1176
968
|
if (patchCalls > 0) {
|
|
1177
969
|
if (patchCalls !== 1) {
|
|
@@ -1245,9 +1037,9 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1245
1037
|
responseCommitted = true;
|
|
1246
1038
|
bootstrapContinuation = undefined;
|
|
1247
1039
|
rehydrationPhase = "step";
|
|
1248
|
-
enqueueTurnPublication(
|
|
1040
|
+
enqueueTurnPublication();
|
|
1249
1041
|
if (snapshot.meta.remotePublication?.mode === "turn-end")
|
|
1250
|
-
|
|
1042
|
+
publicationWorker.launch();
|
|
1251
1043
|
completedRunAccepted = !wasBootstrap;
|
|
1252
1044
|
}
|
|
1253
1045
|
catch (error) {
|
|
@@ -1279,7 +1071,7 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1279
1071
|
startStateFlow(ctx);
|
|
1280
1072
|
}
|
|
1281
1073
|
if (snapshot.meta.remotePublication?.mode === "turn-end")
|
|
1282
|
-
|
|
1074
|
+
publicationWorker.launch();
|
|
1283
1075
|
if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap || resolutionPending
|
|
1284
1076
|
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
|
|
1285
1077
|
|| !shouldRequestStateFlowCompaction(ctx.getContextUsage()))
|
|
@@ -1304,7 +1096,7 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1304
1096
|
restoreActiveBranch(ctx, event.reason);
|
|
1305
1097
|
retryPendingPush(ctx);
|
|
1306
1098
|
if (snapshot.meta.remotePublication?.mode === "turn-end")
|
|
1307
|
-
|
|
1099
|
+
publicationWorker.launch();
|
|
1308
1100
|
updateUi(ctx);
|
|
1309
1101
|
void telegram.ensure();
|
|
1310
1102
|
});
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { isJsonValue, isObject, sameJson, validatePatch } from "./json.js";
|
|
3
3
|
export const RECENT_TRANSITION_LIMIT = 7;
|
|
4
4
|
const SCOPES = new Set(["global", "cwd", "session"]);
|
|
5
|
-
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response", "lazy"]);
|
|
5
|
+
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "intents", "response", "lazy"]);
|
|
6
6
|
/** Normalize accepted replacements into recursive-merge replay, including removals. */
|
|
7
7
|
function replayPatch(before, after) {
|
|
8
8
|
const entries = [];
|