@llblab/pi-kit 0.37.0 → 0.38.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 +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-state-flow/README.md +30 -14
- package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
- package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
- package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
- package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
- package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
- package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
- package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
- package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
- package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
- package/node_modules/@llblab/pi-state-flow/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
- package/package.json +3 -3
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
This is a maintained property-to-test map for the current canonical-file contract, not a second backlog or a substitute for running tests. Paths and quoted test names refer to a source checkout; tests are not shipped in the npm runtime package. Some test labels retain internal Start/Stop terminology: activation uses `/state-flow-active`; inactive scenarios explicitly select Passive or Off according to their tools/context assertions.
|
|
4
4
|
|
|
5
|
+
## Native seven-selection integration
|
|
6
|
+
|
|
7
|
+
The seven-core group in `tests/extension.test.ts` retains all seven × Active/Passive original-ID writes/queries/history/no-op/atomic-refusal, Session-only response capture, reload, selected Stop/Passive/Off context, captured defaults and old-tree current-memory witnesses. Unloaded/Off status must report captured scopes without acquiring memory. The seven-fork group in `tests/extension.test.ts` covers selected parent-current copy, shared-only deferred child/pending preservation, late direct-parent Session acquisition and acquired child-current/no recopy. Its native-policy DATA adapters are not public scope controls or acquisition grants.
|
|
8
|
+
|
|
9
|
+
The seven-facility group in `tests/extension.test.ts` natively creates registrations/provenance before selected source planning, ordinary invalidations/removal and read-driven Skill compilation. Excluded ordinary native reads remain legal; excluded State Flow hints/hash/relocation and semantic-owner mutation do not. Generic maintenance uses presence/fingerprints, not automatic content hashing. The seven-shared-peers group in `tests/extension.test.ts` creates both peers through the SDK and preserves untargeted shared drift without a private owner.
|
|
10
|
+
|
|
11
|
+
The seven-cancellation group in `tests/extension.test.ts` admits each selected Start, then uses a fixture-owned native Off-tree scope DATA adapter for ABA to prove withdrawal before lock release and fresh-owner recovery. The seven-publication-cancellation group in `tests/extension.test.ts` uses actual tool/message events to hold each selected patch/response publication; public Off cancels owned waits while preserving earlier accepted preparation, native answer delivery and independent fresh work. The native scope-lifetime group in `tests/extension.test.ts` separately proves same-mode pending-restoration ABA and equal-policy observation without awaiting a newly enabled replacement while retaining its required exclusion. These are bounded exact-path/SDK functional witnesses, not model-quality, confidentiality or universal syscall/ancestry proofs.
|
|
12
|
+
|
|
13
|
+
## Public scope controls and mode-only Telegram
|
|
14
|
+
|
|
15
|
+
The public-scope group in `tests/extension.test.ts` proves public command tokens, not policy-DATA adapters, select all seven sets from configured Off/all-three; malformed inputs fail atomically, defaults remain unchanged and mode-only/reload retain choice. The public-scope-lifetime group in `tests/extension.test.ts` adds actual public scope ABA, equal canonical held-patch preservation, Off exact-canonical-path I/O guards, different-selection inactive-persistence withdrawal, both late Passive observation kinds and acquired/deferred/fenced refusal. `tests/selection.test.ts` owns full-prefix unused-owner completion; `tests/extension.test.ts` owns parser-before-effects command registration. Requested status and available materialization remain separate exact proofs.
|
|
16
|
+
|
|
17
|
+
`tests/native-telegram-selection.test.ts` publicly captures each of the seven sets, creates native original-ID memory, then invokes the actual optional registered section handlers for mode-only Passive/Off/Active before and after default changes/reload. Mode payload text cannot become a selector; excluded bytes/private absence, native model tools/context, response ownership and explicit disposable Off inspection remain correct. Only the presentation registry/edit/ack/Rich callbacks are fake; native SDK/mode owners/storage are real. Assertions on collected provider inputs/results run outside callbacks.
|
|
18
|
+
|
|
19
|
+
The native-telegram-lifetime group in `tests/extension.test.ts` adds seven held-Start → Telegram Off withdrawals with exact canonical-path guards, seven accepted-Passive receipt → public scope-ABA stale-menu refusals, and two real busy native-operation deferrals (accepted same choice versus newer Off changing scopes). One pure copy/radio-layout property distinguishes operator Effective from participating model owners without adding a chooser. `tests/telegram.test.ts` retains optional registration/failure/inspection/fence/lifetime checks; its CAS-failure inspector now explicitly observes the peer's newer field without installing cache or clearing the write fence. No real Telegram delivery or rendered-device claim follows.
|
|
20
|
+
|
|
21
|
+
See the [M2 functional boundary](scope-selection-contract.md#m2-functional-acceptance-boundary) for the complete property/owner map, legacy-v1 full-cohort dependency, acquired-private presence requirement and explicit-operator exception. These deterministic fixtures are not exhaustive interleavings or universal alias/async filesystem nonaccess.
|
|
22
|
+
|
|
5
23
|
## Required properties and witnesses
|
|
6
24
|
|
|
7
25
|
### 1. Zero patches
|
|
@@ -309,7 +327,10 @@ This is a maintained property-to-test map for the current canonical-file contrac
|
|
|
309
327
|
### 44. Intent-owned memory
|
|
310
328
|
|
|
311
329
|
- `tests/ownership.test.ts` proves same-scope object-key eligibility, recursive structured-reference extraction, shared/ancestor/descendant protection, minimal cascade sets and prototype-safe deletion patches. Textual mentions do not own targets.
|
|
312
|
-
- `tests/transition.test.ts`
|
|
330
|
+
- `tests/transition.test.ts` now proves intents → cleanup → other authored planes for same-scope strategy/tactics/lazy, fresh same-path writes, supersession, edit-only behavior and no-op repeats. The exact one-cold-copy and rejected-draft detachment checks remain. The former authored-first ordering is superseded; `cascaded` records intermediate clears, not final deletion commands, and final effective values govern.
|
|
331
|
+
- `tests/transition.test.ts` includes the selected properties covering seven selections × retention0/1/7/100 × two JSON key orders (56 cohorts), clearing old fields before all-three-plane rewrites, immutable current state and exact final/previous replay. Eighteen plane × related-path × surviving/new-owner cases preserve protected targets; a later invalid owner rejects the whole cohort without publication.
|
|
332
|
+
- New `native intent-first rewrite` cases in `tests/extension.test.ts` cover all seven choices through actual SDK dispatch, original declared arguments with intents deliberately last, reload and retained fresh values. The runtime inspector's model-visible empty intents/lazy omission is not mistaken for canonical absence: actual owned checkpoint/tail replay checks sparse intent removal and fresh lazy content. Source7/7 and import-only actual-DIST7/7 are bounded scripted-provider evidence, not model reasoning or external fact proof.
|
|
333
|
+
- The completed full coordinator gate passed1621/1621 plus build/typecheck/import/prepack dry-run packaging. Initial independent review executed129 focused,29 guidance,7 Source-SDK,7 actual DIST and3 additional DATA counterexamples; counts overlap the full corpus and are not added. It requested a real guidance correction rather than an engine fix. Subsequent actual terminal follow-ups verified the handoff/backlog lifetime and support-policy prose; initial dissent remains retained, not relabeled earlier approval. Final documentation-only approval was consumed after ROOT terminal before closure; unchanged engine evidence is reused, not rerun or recounted.
|
|
313
334
|
- `tests/temporal.test.ts` proves replay equality from explicit stored deletions through retained history and checkpoint folding; replay does not re-derive ownership.
|
|
314
335
|
- `tests/context.test.ts` requires deleted hot paths and changed lazy navigation in the receipt while the frozen head remains untouched.
|
|
315
336
|
- Per-scope native cases in `tests/integration.test.ts` exercise the cascade in both Active and Passive, preserve textual-reference targets and check one revision per patch cohort. Active response reconciliation is a separate Session revision.
|
|
@@ -43,12 +43,16 @@ Active, Passive and Off select the session's workflow and model-facing access, n
|
|
|
43
43
|
**`/state-flow-active`** activates the current Pi branch over validated current same-session memory, initializing absent storage when safe. No remote is required.
|
|
44
44
|
|
|
45
45
|
- Starting mid-conversation keeps Pi's active context for one complete bootstrap run, during which the agent must compile future-relevant information into state.
|
|
46
|
-
- Repeating Active
|
|
46
|
+
- Repeating Active with the same captured scopes leaves the in-progress run unchanged; a different explicit selection is an owned reselection, not an already-active no-op.
|
|
47
47
|
- On an attached branch, activation waits asynchronously for a coherent canonical cohort; pending repeats share the wait.
|
|
48
48
|
- Until acceptance, the existing inactive mode and any write fence stay in effect.
|
|
49
49
|
- Cancelling a pending Active from Off preserves later acquisition choices; ordinary cancellation creates no write fence.
|
|
50
50
|
- Passive, Off or a branch selection can withdraw an obsolete activation. An error after acceptance does not undo accepted memory.
|
|
51
51
|
|
|
52
|
+
All three mode commands accept optional exact owner tokens, in any order: `/state-flow-active session`, `/state-flow-passive cwd global`, `/state-flow-off cwd session`. Any nonempty unique subset of `global cwd session` is valid; autocomplete offers only unused matching tokens and replaces the full argument prefix. Blank/whitespace-only arguments preserve the captured choice. Unknown/duplicate names, flags and comma-separated forms fail before policy or mode changes. Commands never edit global defaults. Off scope changes record native policy only, without reading or initializing canonical memory. Active explicitly owns acquisition where independently permitted; Passive observation alone cannot acquire missing Session or borrow pending fork permission.
|
|
53
|
+
|
|
54
|
+
Selection is not materialization: all three scopes are selected by default, but a pre-runtime Passive session can have no private Session cohort yet. Read-only observation exposes existing shared owners without inventing empty Session state. An explicit read of a selected owner missing from that read view reports `selected, unavailable`, not `excluded`; an actually excluded owner is rejected by the selection guard. A legitimate first Session patch on an eligible non-fork branch can acquire private memory through normal atomic acceptance. Do not send an empty patch just to initialize it or bypass pending-fork authority.
|
|
55
|
+
|
|
52
56
|
How each lifecycle event behaves:
|
|
53
57
|
|
|
54
58
|
- **New session:** Adopts the global `mode`, defaulting to Off.
|
|
@@ -145,6 +149,7 @@ The canonical store is `state-flow/` beneath the agent directory. Keeping config
|
|
|
145
149
|
- Active initializes missing storage when safe.
|
|
146
150
|
- Passive reads existing memory without publishing; its first explicit patch may initialize absent storage without starting an episode or compaction.
|
|
147
151
|
- Off exposes neither memory tools nor State Flow context.
|
|
152
|
+
- `scopes`: Missing means `global`, `cwd`, `session`. Any of the seven nonempty unique subsets is supported; order is canonicalized without rewriting the file. For example, `"scopes": ["cwd"]` selects project-only memory. The default is captured once for a new or not-yet-captured legacy branch; an existing choice survives reload, tree navigation and later default edits, and forks inherit their parent branch's choice. Mode-only commands/Telegram actions preserve it. Use explicit owner tokens on an existing mode command to change only the current branch's choice; a settings chooser is not required. Never edit accepted lifecycle files to select scopes. Disabled owners cannot be read/patched as model memory or relocated into another owner. Without Session, native answers are delivered but not stored/backfilled; a fork defers private acquisition rather than creating a hidden Session stream. See [native seven-selection integration](scope-selection-contract.md#retained-native-seven-selection-integration) and the [capture boundary](scope-selection-contract.md#configured-scope-defaults-and-native-capture).
|
|
148
153
|
- `logging`: Defaults to `false`. When enabled, rejected patch executions, active barrier blocks, preparation failures and accepted-answer reconciliation failures are recorded locally at `tmp/pi-state-flow/logs.jsonl` beneath the agent directory. Older `tmp/state-flow` logs are left untouched and are not migrated. Asynchronous Git push failures are recorded there even when this setting is off.
|
|
149
154
|
- `showSuccessfulPatches`: Defaults to `true`. In interactive Pi, `patch_state` rows show pretty-printed JSON arguments once in the call, with blank lines between adjacent memory sections; the result shows the acceptance or no-op acknowledgement, never a second patch. Set it to `false` to hide call arguments and keep the compact acknowledgement. Rejected calls retain their arguments and error acknowledgement; State Flow adds no private validation turn.
|
|
150
155
|
- `historyLimit`: Defaults to `7` and accepts integers from `0` through `100`. It counts accepted semantic transitions, **not elapsed time or conversation length**, and bounds materialized-history and scope patch-history offsets.
|
|
@@ -152,9 +157,9 @@ The canonical store is `state-flow/` beneath the agent directory. Keeping config
|
|
|
152
157
|
- Zero keeps only current checkpoints.
|
|
153
158
|
- Raising it affects only future retention and cannot reconstruct discarded history.
|
|
154
159
|
|
|
155
|
-
**Session configuration.** A session's `config.json` uses the same `mode` key for its concrete choice;
|
|
160
|
+
**Session configuration.** A session's `config.json` uses the same `mode` key for its concrete choice; accepted v2 lifecycle also records concrete `scopes`. Commands and Telegram change that session only and do not edit global defaults.
|
|
156
161
|
|
|
157
|
-
- Before canonical runtime acceptance,
|
|
162
|
+
- Before canonical runtime acceptance, an owner-bound native v2 policy record captures `{mode, scopes}` instead of a manufactured storage pair or accepted semantic boundary. A captured branch/fork choice survives later global-default edits.
|
|
158
163
|
- Off stays native-only even after an accepted runtime: its mode/bookmark overrides the earlier canonical mode without needing store access or granting semantic authority.
|
|
159
164
|
- Edit global defaults manually or through an authorized agent, not through `patch_state`.
|
|
160
165
|
- Legacy flag decoding is read-only; see [mode compatibility](compatibility.md#mode-configuration-compatibility).
|
|
@@ -190,10 +195,10 @@ Treat logs and state files as private. Removing a secret from current state does
|
|
|
190
195
|
|
|
191
196
|
`/state-flow-status` separates runtime configuration/metadata from semantic state. It reports:
|
|
192
197
|
|
|
193
|
-
-
|
|
198
|
+
- Captured selected owners and, when different, available materialized owners; their known keys, local run step, exact available owner-revision vector and hot-history depth. Unavailable owners receive no fabricated value or zero revision.
|
|
194
199
|
- Known recovery or publication failures, actual artifact counts when nonzero, and pending invalidations. Status does not discover or validate sources.
|
|
195
|
-
- A `Scope memory:` block with one line per scope that has content: the UTF-8 byte size of each present nonempty plane and, when `
|
|
196
|
-
- One JSON representation of
|
|
200
|
+
- A `Scope memory:` block with one line per scope that has content: the UTF-8 byte size of each present nonempty plane and, when `tactics` or `lazy` has top-level entries, the share owned by an open intent of that scope (for example `- cwd: intents 137 B, tactics 32 B; intent-owned tactics 1/2`). It is operator-only: no notice, threshold or model-facing effect.
|
|
201
|
+
- One JSON representation of available selected memory in relative global → CWD → session order, with a blank line between top-level semantic planes. Nested JSON is unchanged; individual scope JSON remains available through `read_state`.
|
|
197
202
|
|
|
198
203
|
Status reports the selected mode and any Active restoration inference fence, while omitting generic ownership/configuration prose. Failed inspection reports unavailable evidence, not invented empty state. Status is observational: it does not read source files, calculate fingerprints, create invalidations or mutate semantic state.
|
|
199
204
|
|
|
@@ -201,7 +206,7 @@ Status reports the selected mode and any Active restoration inference fence, whi
|
|
|
201
206
|
|
|
202
207
|
- The terminal indicator is accent `state-flow` plus dim `active` or `passive`; Off hides it. Failed Active restoration stays visible as `active (blocked)`, never as an apparently inactive mode.
|
|
203
208
|
- Global, CWD and Session own independent semantic revisions. One atomic transition advances each materially changed scope once, including session-only response reconciliation.
|
|
204
|
-
- Effective has no scalar counter. It uses the compact lowercase
|
|
209
|
+
- Effective has no scalar counter. It uses the compact lowercase available-owner revision vector (for example, `g15c8s31` for all three or `c8` for only CWD), with no slashes, spaces or invented excluded counters. The vector appears in `/state-flow-status` and in Telegram's Effective inspection, not in the compact indicators.
|
|
205
210
|
|
|
206
211
|
### Telegram controls
|
|
207
212
|
|
|
@@ -210,7 +215,8 @@ When `pi-telegram` is available, its main-menu section shows `State Flow: active
|
|
|
210
215
|
**Mode controls:**
|
|
211
216
|
|
|
212
217
|
- A horizontal radio row presents `Off | Passive | Active`. The selected option uses 🟣, 🟡 or 🟢 respectively; each inactive option uses ⚫️.
|
|
213
|
-
-
|
|
218
|
+
- All three mode buttons preserve the current captured scope set, even if callback payload text names another owner. They are not a participation chooser; use explicit owner arguments on the existing commands when changing participation.
|
|
219
|
+
- Telegram Active requested during a run waits for settlement; Passive and Off apply immediately. Off withdraws an old deferred Start, and scope changes cannot revive it when settlement arrives.
|
|
214
220
|
- The first accepted Passive click refreshes the selected button after memory restoration or write-fence recovery, including while the agent is busy. A later mode or branch selection still suppresses an obsolete menu update.
|
|
215
221
|
- Ordinary successful controls add no redundant mode receipt; diagnostic outcomes stay visible.
|
|
216
222
|
- Off exposes neither tool nor State Flow model context.
|
|
@@ -220,24 +226,27 @@ When `pi-telegram` is available, its main-menu section shows `State Flow: active
|
|
|
220
226
|
- The submenu heading shows the current value in monospace.
|
|
221
227
|
- The bold Mode heading uses a long dash, a description ending in a colon, and a blank line before its settings-style list. Each line has a monospaced minus and lowercase monospaced mode value, then a plain colon and description.
|
|
222
228
|
- The descriptions progress from regular chat with no memory (Off, the new-session default), to ordinary chat with memory tools and an available combined memory view (Passive), to the same memory access with completed answers followed by memory-first continuation (Active). They do not claim a new physical Pi session or disabled native compaction.
|
|
223
|
-
-
|
|
224
|
-
-
|
|
229
|
+
- A Participating scopes line shows the captured current choice, initially from global defaults; later default edits do not replace an existing session choice. Mode/help text describes only that set and states when excluded Session means new answers are delivered without storage.
|
|
230
|
+
- The bold Inspect participating memory heading follows the same long-dash, description, colon and blank-line pattern. Only selected owners get a monospaced description and direct button; this is not a scope chooser.
|
|
231
|
+
- With one selected owner, its state is also effective and no separate Effective button appears. With two or three, Effective shows the validated available overlay of those selected owners only, in Global→CWD→Session order. Excluded owners do not contribute or gain an inspection action.
|
|
225
232
|
|
|
226
233
|
**Memory inspection:**
|
|
227
234
|
|
|
228
235
|
- Owner-scope Rich snapshots show `#revision`; Effective shows the vector.
|
|
229
236
|
- Fields show pretty-printed JSON directly. Large fields show a bounded JSON prefix with a separate truncation notice and omitted-character count, not a JSON-encoded `preview` string. The preview may end mid-value; actual escapes inside JSON string values remain intact. These presentation limits never truncate stored memory or successful `read_state` results.
|
|
230
|
-
-
|
|
231
|
-
-
|
|
237
|
+
- A failed-Passive write fence does not grant repair or cached fallback: explicit operator inspection still validates current canonical memory through a disposable reader. Peer-accepted current values remain observable without clearing the fence or installing a model cache.
|
|
238
|
+
- Inspection waits cancelably for its required current authority, even when model tools are disabled. It does not initialize, publish or advance storage.
|
|
232
239
|
- Data and displayed revisions come from the same observation; absent or invalid memory stays unavailable.
|
|
233
240
|
- Stop, session/tree changes and shutdown cancel obsolete observations.
|
|
234
241
|
- The button acknowledges immediately, and a late failure appears in the menu instead of an expired callback popup.
|
|
235
242
|
|
|
236
243
|
**Inspecting while Off.** Explicit Off inspection reads current stored memory through a disposable reader.
|
|
237
244
|
|
|
238
|
-
- Session and Effective require validated same-session private authority
|
|
245
|
+
- Session, and any Effective view containing selected Session, require the existing validated same-session private authority; a missing private layer/revision is not fabricated. A selected shared-only Effective view uses shared observation and does not read excluded Session bodies. Legacy lifecycle proof may still require its documented complete cohort.
|
|
239
246
|
- These reads install no model/runtime cache, change no bytes or mode, clear no write fence, and leave pending fork acquisition intact for a future Passive/Active selection.
|
|
240
|
-
- Automatic Off callbacks never acquire memory.
|
|
247
|
+
- Automatic Off callbacks never acquire memory. Explicit inspection is read-only even in Off, but the Telegram surface still restricts targets to the captured owners. Obsolete excluded-owner or singleton-Effective callbacks reject before invoking readers; selection changes during acknowledgement/observation withdraw stale results. This presentation rule is not confidentiality, transcript erasure or private-acquisition authority.
|
|
248
|
+
|
|
249
|
+
The [M2 functional acceptance map](scope-selection-contract.md#m2-functional-acceptance-boundary) records deterministic SDK/presentation witnesses and unavoidable legacy/full-proof dependencies. Scripted provider plus fake presentation is not actual Telegram delivery or live-model quality evidence.
|
|
241
250
|
|
|
242
251
|
Open implementation work is tracked in [BACKLOG.md](../BACKLOG.md).
|
|
243
252
|
|
|
@@ -332,11 +341,12 @@ Commit or push failure is diagnostic-only; repeated push failures warn once per
|
|
|
332
341
|
|
|
333
342
|
**Supported formats.** State Flow accepts only canonical checkpoint/tail files, temporal metadata and the separate session config/runtime contract.
|
|
334
343
|
|
|
335
|
-
-
|
|
336
|
-
-
|
|
344
|
+
- Scope metadata versions 1/2 remain readable through explicit `contract → strategy`, `working → tactics` translation before field filtering. Current/historical tools expose only the target dialect; retired root writes reject rather than becoming aliases. Legacy missing revisions credit only retained tail records.
|
|
345
|
+
- New writes use scope metadata version 3. On an owned semantic acceptance, legacy cohorts convert with unchanged semantic revisions/causal identities, under the existing lock/CAS and rollback checks. Ordinary inspection and runtime-only mode persistence do not convert files. Upgrade/quiesce all writers before enabling the new code: already-deployed low-level readers cannot be universally fenced by a format number.
|
|
337
346
|
- Unsupported checkpoint envelopes, combined session metadata, `state.json`, hashed layouts and semantic Pi checkpoint envelopes fail as unsupported, without rewriting existing bytes.
|
|
338
347
|
- Canonical semantic objects lacking optional fields, such as `intents` or `lazy`, are valid sparse state and do not cross this boundary.
|
|
339
|
-
-
|
|
348
|
+
- Before any v1/v2-to-v3 canonical publication, the storage owner writes and verifies an exact-byte JSON backup at `<parent-of-store>/<store-name>-migration-backups/<sha256>.json` (created with directory mode 0700, file mode 0600 on the verified platform; existing non-private paths reject). It lists converted scopes and updated paths alongside the transaction's owned file basis, relative paths, identities, Base64 originals and explicit missing entries, including required Session config/runtime. It never discovers unrelated projects/sessions or source bodies. The returned publication exposes `migrationBackup`; completed backups survive rollback and exact retries reuse them. Backup failure blocks canonical writes.
|
|
349
|
+
- This backup is recovery evidence outside canonical authority, not a Git commit, journal, automatic fallback or power-loss guarantee. Quiesce writers and request a reviewed complete-cohort recovery before downgrade/restoration; do not overwrite intervening foreign data or restore just one file. Retain backups privately. State Flow 0.x retains legacy conversion and retired-root reservations; the temporary support window ends at State Flow 1.0.0, with unsupported versions still explicitly rejected. The prepared0.27.0 candidate keeps its converter/reservations unchanged; no automatic removal, release or working-store migration follows. See the [settled support policy](semantic-compatibility-decision.md#implementation-direction-and-reserved-names).
|
|
340
350
|
|
|
341
351
|
### Conflicts and interrupted publication
|
|
342
352
|
|
|
@@ -375,6 +385,10 @@ No automatic crash repair or power-loss durability is promised, and Git backup h
|
|
|
375
385
|
|
|
376
386
|
## Lazy navigation and historical reading
|
|
377
387
|
|
|
388
|
+
With exactly one captured participating owner, `read_state` needs no scope token: `lazy.releasePlan` reads current data, `effective[3].lazy.releasePlan` selects its retained older boundary, and a scalar `patches[0]` reads its latest retained owner patch. Ordinary `lazy.items[3]` still indexes an array. Optional explicit selected-owner paths remain legal. With multiple wanted owners, ordinary flat paths use the effective overlay and owner patch history requires `owner.patches[N]`; sparse available memory does not turn a multi-owner choice into one-owner mode. Unavailable history/owners still fail rather than returning invented empty data.
|
|
389
|
+
|
|
390
|
+
Returned current same-owner structured refs may be scope-free; persisted refs retain canonical ownership. For one wanted owner, `patch_state` now takes planes directly (for example `{ "tactics": { "ready": true } }`) and anchors eligible refs after native admission; do not add an owner wrapper. For multiple wanted owners, keep explicit participating owner patches in one atomic call. Canonical ownership/replay never become scope-free, and changing schema cannot admit an old call under a new owner. Single-owner Active/Passive/bootstrap instructions and packaged Skills use that same grammar. Active/Passive heads and projection-bound updates now render eligible same-owner current refs relatively; the lazy catalog points to `lazy` for one WANTED owner and `effective.lazy` for several. Active recent context uses `{id,at,patch}` for one owner and explicit `transitions` for several, retaining IDs/positions while omitting lazy bodies. These are read-only context DTOs, not patch arguments or storage/replay formats. Canonical provenance, source paths, history and cross-owner/resource refs remain intact. Registered Skill acquisition targets use direct `artifacts[exact source key]` in one-owner mode, explicit owners in multi mode; this target syntax is not a `read_state` locator. Deletion text uses relative JSON pointers such as `/tactics/missing` for one owner, while result details retain canonical owners/paths. Newly generated Stop/reload/bootstrap handoffs render eligible current refs in the same captured interface. Canonical compiler diagnostics may name provenance owners; those are not extra patch wrappers, and retries use the declared flat planes. Exact source keys and canonical operator/error evidence are never broadly prefix-stripped. See the [context presentation boundary](single-scope-interface-contract.md#runtime-context-presentation).
|
|
391
|
+
|
|
378
392
|
Lazy bodies require explicit reads:
|
|
379
393
|
|
|
380
394
|
- Automatic state and recent-transition projections omit them, including lazy deletions. Bounded `lazy_navigation` can still show the current layer's presence, path and key types.
|
|
@@ -396,7 +410,7 @@ Lazy bodies require explicit reads:
|
|
|
396
410
|
- A proven stale reference may be repaired within touched work, without resurrecting its target.
|
|
397
411
|
- Without a match, ordinary missing-path errors remain; keys, patch and batch projections keep their existing contracts.
|
|
398
412
|
|
|
399
|
-
Intent-owned
|
|
413
|
+
Intent-owned strategy, tactics or lazy object keys are cleared after applying intents and before remaining authored planes, independent of JSON insertion order. Final remaining/new same-scope intent references protect direct and ancestor/descendant targets. Rewrite survivors at the same/new path in the closing patch; cleared targets retain no old fields. `cascaded` reports intermediate cleared paths even if recreated; final effective values and retained explicit net patches describe actual state. Historical replay never reruns cascade or rewrites old history. See [intent ownership](lazy-state.md#intent-ownership).
|
|
400
414
|
|
|
401
415
|
Artifact freshness/invalidations and optional Skill acquisition hints keep their exact source/scope targets. Hints expose possibilities and diagnostic evidence; the current task decides whether action is necessary.
|
|
402
416
|
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
type ArtifactSourceIdentity,
|
|
9
9
|
ORDINARY_ARTIFACT_COMPILER,
|
|
10
10
|
type ArtifactProvenanceRegistry,
|
|
11
|
+
type ArtifactMetadata,
|
|
11
12
|
} from "./artifact.ts";
|
|
12
|
-
import { isObject, type JsonObject } from "./json.ts";
|
|
13
|
-
import
|
|
13
|
+
import { isObject, sameJson, type JsonObject } from "./json.ts";
|
|
14
|
+
import { assertScopeStateValues, type AtomicScopePatches, type ScopeStateValues, type StateScope } from "./state.ts";
|
|
15
|
+
import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, type ScopeSelection } from "./selection.ts";
|
|
14
16
|
|
|
15
17
|
/** Why the caller is considering source-body acquisition. */
|
|
16
18
|
export type ArtifactAcquisitionIntent =
|
|
@@ -52,6 +54,8 @@ interface PendingRead {
|
|
|
52
54
|
toolName: string;
|
|
53
55
|
args: unknown;
|
|
54
56
|
fingerprint?: ArtifactSourceFingerprint;
|
|
57
|
+
path?: string;
|
|
58
|
+
candidate?: ArtifactInvalidationRequest;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
function readPath(toolName: unknown, args: unknown): string | undefined {
|
|
@@ -66,9 +70,29 @@ export class ArtifactReadTracker {
|
|
|
66
70
|
readonly #pending = new Map<string, PendingRead>();
|
|
67
71
|
readonly #candidates = new Map<string, ArtifactInvalidationRequest>();
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
#selection = DEFAULT_SCOPE_SELECTION;
|
|
74
|
+
|
|
75
|
+
setCandidates(candidates: Iterable<ArtifactInvalidationRequest>, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION): void {
|
|
76
|
+
const wanted = captureScopeSelection(selection);
|
|
77
|
+
const next = new Map<string, ArtifactInvalidationRequest>();
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
if (!wanted.includes(candidate.scope ?? "global")) continue;
|
|
80
|
+
const captured = structuredClone(candidate);
|
|
81
|
+
for (const key of ["scope", "hash", "sourceFingerprint"] as const) if (captured[key] === undefined) delete captured[key];
|
|
82
|
+
next.set(candidate.path, captured);
|
|
83
|
+
}
|
|
84
|
+
if (!sameJson(wanted, this.#selection)) this.clear();
|
|
85
|
+
this.#selection = wanted;
|
|
86
|
+
for (const [path] of this.successful) {
|
|
87
|
+
const previous = this.#candidates.get(path), current = next.get(path);
|
|
88
|
+
if (previous === undefined || current === undefined || !sameJson(previous, current)) this.successful.delete(path);
|
|
89
|
+
}
|
|
70
90
|
this.#candidates.clear();
|
|
71
|
-
for (const candidate of
|
|
91
|
+
for (const [path, candidate] of next) this.#candidates.set(path, candidate);
|
|
92
|
+
for (const [id, pending] of this.#pending) {
|
|
93
|
+
const current = pending.path === undefined ? undefined : next.get(pending.path);
|
|
94
|
+
if (current === undefined || pending.candidate === undefined || !sameJson(current, pending.candidate)) this.#pending.delete(id);
|
|
95
|
+
}
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
clear(): void {
|
|
@@ -89,9 +113,9 @@ export class ArtifactReadTracker {
|
|
|
89
113
|
this.#pending.delete(toolCallId);
|
|
90
114
|
if (isError || !pending || toolName !== pending.toolName) return;
|
|
91
115
|
const path = readPath(pending.toolName, pending.args);
|
|
92
|
-
if (path === undefined) return;
|
|
116
|
+
if (path === undefined || path !== pending.path) return;
|
|
93
117
|
const candidate = this.#candidates.get(path);
|
|
94
|
-
if (candidate === undefined || pending.fingerprint === undefined) return;
|
|
118
|
+
if (candidate === undefined || pending.candidate === undefined || pending.fingerprint === undefined || !sameJson(candidate, pending.candidate)) return;
|
|
95
119
|
const observation = inspectRegisteredArtifactPaths([path])[0];
|
|
96
120
|
if (observation?.kind !== "present" || !sameArtifactSourceFingerprint(pending.fingerprint, observation.fingerprint)) return;
|
|
97
121
|
const finalObservation = inspectRegisteredArtifactPaths([path])[0];
|
|
@@ -108,7 +132,7 @@ export class ArtifactReadTracker {
|
|
|
108
132
|
const observation = path !== undefined && this.#candidates.has(path) ? inspectRegisteredArtifactPaths([path])[0] : undefined;
|
|
109
133
|
this.#pending.set(toolCallId, {
|
|
110
134
|
toolName,
|
|
111
|
-
args,
|
|
135
|
+
args, path, candidate: path === undefined ? undefined : this.#candidates.get(path),
|
|
112
136
|
...(observation?.kind === "present" ? { fingerprint: observation.fingerprint } : {}),
|
|
113
137
|
});
|
|
114
138
|
}
|
|
@@ -160,8 +184,6 @@ export function decideArtifactAcquisition(
|
|
|
160
184
|
|
|
161
185
|
export const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
|
|
162
186
|
|
|
163
|
-
const SCOPES = ["global", "cwd", "session"] as const;
|
|
164
|
-
|
|
165
187
|
/**
|
|
166
188
|
* One selected branch's ordinary-artifact acquisition plan: runtime-observed
|
|
167
189
|
* invalidations, their model hints and the read tracker correlated to them.
|
|
@@ -171,6 +193,7 @@ export class ArtifactAcquisitionState {
|
|
|
171
193
|
readonly reads = new ArtifactReadTracker();
|
|
172
194
|
#invalidations: ArtifactInvalidationRequest[] = [];
|
|
173
195
|
#hints: Record<string, string> = {};
|
|
196
|
+
#selection = DEFAULT_SCOPE_SELECTION;
|
|
174
197
|
|
|
175
198
|
get invalidations(): readonly ArtifactInvalidationRequest[] { return this.#invalidations; }
|
|
176
199
|
get hints(): Record<string, string> { return this.#hints; }
|
|
@@ -185,29 +208,29 @@ export class ArtifactAcquisitionState {
|
|
|
185
208
|
}
|
|
186
209
|
|
|
187
210
|
/** Re-observe exact registered sources for the selected scope artifacts. */
|
|
188
|
-
refresh(states:
|
|
189
|
-
const
|
|
190
|
-
|
|
211
|
+
refresh(states: ScopeStateValues, provenance: (scope: StateScope) => ArtifactProvenanceRegistry, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION): void {
|
|
212
|
+
const wanted = captureScopeSelection(selection);
|
|
213
|
+
assertScopeStateValues(states, wanted);
|
|
214
|
+
const effective = new Map<string, { scope: StateScope; metadata: ArtifactMetadata }>();
|
|
215
|
+
for (const scope of wanted) for (const [path, metadata] of Object.entries(states[scope]!.artifacts ?? {})) effective.set(path, { scope, metadata });
|
|
216
|
+
const paths = [...effective].filter(([, { metadata }]) => metadata.kind !== "skill").map(([path]) => path);
|
|
191
217
|
const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
|
|
192
218
|
const hints: Record<string, string> = {};
|
|
193
|
-
const invalidations =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (need.kind !== "requires-compilation") continue;
|
|
205
|
-
if (need.reason === "source-changed") hints[path] = SOURCE_CHANGED_HINT;
|
|
206
|
-
invalidations.set(path, { path, scope, reason: need.reason });
|
|
207
|
-
}
|
|
219
|
+
const invalidations: ArtifactInvalidationRequest[] = [];
|
|
220
|
+
const registries = new Map<StateScope, ArtifactProvenanceRegistry>();
|
|
221
|
+
for (const path of paths) {
|
|
222
|
+
const { scope, metadata } = effective.get(path)!;
|
|
223
|
+
const observed = observations.get(path);
|
|
224
|
+
if (observed?.kind !== "present") continue;
|
|
225
|
+
if (!registries.has(scope)) registries.set(scope, provenance(scope));
|
|
226
|
+
const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, registries.get(scope)![path]);
|
|
227
|
+
if (need.kind !== "requires-compilation") continue;
|
|
228
|
+
if (need.reason === "source-changed") hints[path] = SOURCE_CHANGED_HINT;
|
|
229
|
+
invalidations.push({ path, scope, reason: need.reason });
|
|
208
230
|
}
|
|
231
|
+
this.#selection = wanted;
|
|
209
232
|
this.#hints = hints;
|
|
210
|
-
this.#setInvalidations(
|
|
233
|
+
this.#setInvalidations(invalidations.sort((left, right) => left.path.localeCompare(right.path)));
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
/** Accepted compilations leave the plan; correlated read evidence is single-use. */
|
|
@@ -218,14 +241,28 @@ export class ArtifactAcquisitionState {
|
|
|
218
241
|
|
|
219
242
|
#setInvalidations(invalidations: ArtifactInvalidationRequest[]): void {
|
|
220
243
|
this.#invalidations = invalidations;
|
|
221
|
-
this.reads.setCandidates(invalidations);
|
|
244
|
+
this.reads.setCandidates(invalidations, this.#selection);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Exact-path metadata revalidation only; callers acquire source bodies outside canonical exclusion. */
|
|
249
|
+
export function validateArtifactReadEvidence(reads: Iterable<SuccessfulArtifactRead>, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION): void {
|
|
250
|
+
const wanted = captureScopeSelection(selection);
|
|
251
|
+
for (const read of reads) {
|
|
252
|
+
if (!wanted.includes(read.scope ?? "global")) continue;
|
|
253
|
+
const observed = inspectRegisteredArtifactPaths([read.path])[0];
|
|
254
|
+
if (read.sourceFingerprint === undefined || observed?.kind !== "present" || !sameArtifactSourceFingerprint(read.sourceFingerprint, observed.fingerprint)) {
|
|
255
|
+
throw new Error(`Artifact source changed after acquisition: ${read.path}`);
|
|
256
|
+
}
|
|
222
257
|
}
|
|
223
258
|
}
|
|
224
259
|
|
|
225
260
|
/** Deletion patches for registered artifacts whose exact source is observed missing, in every owning scope. */
|
|
226
|
-
export function missingArtifactRemovals(states:
|
|
261
|
+
export function missingArtifactRemovals(states: ScopeStateValues, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION): AtomicScopePatches {
|
|
262
|
+
const wanted = captureScopeSelection(selection);
|
|
263
|
+
assertScopeStateValues(states, wanted);
|
|
227
264
|
const owners = new Map<string, StateScope[]>();
|
|
228
|
-
for (const scope of
|
|
265
|
+
for (const scope of wanted) for (const path of Object.keys(states[scope]!.artifacts ?? {})) {
|
|
229
266
|
owners.set(path, [...owners.get(path) ?? [], scope]);
|
|
230
267
|
}
|
|
231
268
|
const removals: AtomicScopePatches = {};
|
|
@@ -270,42 +270,41 @@ function isProvenanceEntry(value: unknown): value is ArtifactProvenance {
|
|
|
270
270
|
export function parseArtifactProvenanceRegistry(value: unknown, context = "Artifact provenance"): ArtifactProvenanceRegistry {
|
|
271
271
|
if (value === undefined) return {};
|
|
272
272
|
if (!isObject(value)) throw new Error(`${context} must be a path-keyed JSON object`);
|
|
273
|
-
const
|
|
274
|
-
|
|
273
|
+
const known = new Set(["sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt"]);
|
|
274
|
+
return Object.fromEntries(Object.entries(value).map(([path, entry]) => {
|
|
275
275
|
if (path.trim().length === 0) throw new Error(`${context} path keys must be non-empty`);
|
|
276
|
-
if (!isObject(entry) || !isJsonValue(entry)) {
|
|
277
|
-
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
const known = new Set(["sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt"]);
|
|
281
|
-
if (Object.keys(entry).some((key) => !known.has(key))) {
|
|
282
|
-
registry[path] = { malformed: true };
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
registry[path] = {
|
|
276
|
+
if (!isObject(entry) || !isJsonValue(entry) || Object.keys(entry).some(key => !known.has(key))) return [path, { malformed: true }];
|
|
277
|
+
return [path, {
|
|
286
278
|
...(Object.hasOwn(entry, "sourceHash") ? { sourceHash: entry.sourceHash } : {}),
|
|
287
279
|
...(Object.hasOwn(entry, "sourceFingerprint") ? { sourceFingerprint: entry.sourceFingerprint } : {}),
|
|
288
280
|
...(Object.hasOwn(entry, "compilerRevision") ? { compilerRevision: entry.compilerRevision } : {}),
|
|
289
281
|
...(Object.hasOwn(entry, "compiledAt") ? { compiledAt: entry.compiledAt } : {}),
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
return registry;
|
|
282
|
+
}];
|
|
283
|
+
}));
|
|
293
284
|
}
|
|
294
285
|
|
|
295
286
|
/** Canonical retained form; uninterpretable entries cannot round-trip and are omitted. */
|
|
296
287
|
export function serializeArtifactProvenanceRegistry(registry: Readonly<ArtifactProvenanceRegistry>): JsonObject {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (entry.malformed === true) continue;
|
|
288
|
+
return Object.fromEntries(Object.entries(registry).flatMap(([path, entry]) => {
|
|
289
|
+
if (entry.malformed === true) return [];
|
|
300
290
|
const fields: JsonObject = {};
|
|
301
291
|
if (entry.sourceHash !== undefined) fields.sourceHash = entry.sourceHash as JsonValue;
|
|
302
292
|
if (entry.sourceFingerprint !== undefined) fields.sourceFingerprint = entry.sourceFingerprint as JsonValue;
|
|
303
293
|
if (entry.compilerRevision !== undefined) fields.compilerRevision = entry.compilerRevision as JsonValue;
|
|
304
294
|
if (entry.compiledAt !== undefined) fields.compiledAt = entry.compiledAt as JsonValue;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
295
|
+
return Object.keys(fields).length === 0 ? [] : [[path, fields]];
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Eligibility to revoke known evidence, not retained-data admission or a sanitizer. */
|
|
300
|
+
export function isInterpretableArtifactProvenance(entry: unknown): entry is ArtifactProvenance {
|
|
301
|
+
if (!isObject(entry)) return false;
|
|
302
|
+
const keys = Object.keys(entry);
|
|
303
|
+
return keys.length > 0 && keys.every(key => ["sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt"].includes(key))
|
|
304
|
+
&& (!Object.hasOwn(entry, "sourceHash") || isArtifactHash(entry.sourceHash))
|
|
305
|
+
&& (!Object.hasOwn(entry, "sourceFingerprint") || parseArtifactSourceFingerprint(entry.sourceFingerprint) !== undefined)
|
|
306
|
+
&& (!Object.hasOwn(entry, "compilerRevision") || (typeof entry.compilerRevision === "string" && entry.compilerRevision.trim().length > 0))
|
|
307
|
+
&& (!Object.hasOwn(entry, "compiledAt") || typeof entry.compiledAt === "string");
|
|
309
308
|
}
|
|
310
309
|
|
|
311
310
|
function invalidField(value: unknown, validate: (candidate: unknown) => boolean): boolean {
|
|
@@ -5,6 +5,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import { getDurableRepositoryRoot } from "./durable.ts";
|
|
6
6
|
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT } from "./history.ts";
|
|
7
7
|
import { isObject } from "./json.ts";
|
|
8
|
+
import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, type ScopeSelection } from "./selection.ts";
|
|
8
9
|
import { isStateFlowMode, type InactiveMode, type StateFlowMode } from "./snapshot.ts";
|
|
9
10
|
|
|
10
11
|
export interface StateFlowConfig {
|
|
@@ -12,6 +13,8 @@ export interface StateFlowConfig {
|
|
|
12
13
|
directory: string;
|
|
13
14
|
/** Default mode for genuinely new sessions only; each session owns its selected mode. */
|
|
14
15
|
mode: StateFlowMode;
|
|
16
|
+
/** Concrete default selection for genuinely new or not-yet-captured legacy sessions. */
|
|
17
|
+
scopes: ScopeSelection;
|
|
15
18
|
/** Non-active fallback for legacy `enabled:false` evidence and unavailable selections; never serialized. */
|
|
16
19
|
inactiveMode: InactiveMode;
|
|
17
20
|
/** Opt-in local capture of rejected patch attempts and unresolved terminal drafts. */
|
|
@@ -28,6 +31,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
|
|
|
28
31
|
const defaults: StateFlowConfig = {
|
|
29
32
|
directory,
|
|
30
33
|
mode: "off",
|
|
34
|
+
scopes: DEFAULT_SCOPE_SELECTION,
|
|
31
35
|
inactiveMode: "off",
|
|
32
36
|
logging: false,
|
|
33
37
|
showSuccessfulPatches: true,
|
|
@@ -41,7 +45,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
|
|
|
41
45
|
throw new Error(`Cannot read State Flow configuration: ${path}`, { cause: error });
|
|
42
46
|
}
|
|
43
47
|
// autoStart/passiveBootstrap/passiveTools are read-only compatibility inputs; explicit mode is authoritative.
|
|
44
|
-
const allowed = new Set(["mode", "autoStart", "passiveBootstrap", "passiveTools", "logging", "showSuccessfulPatches", "historyLimit"]);
|
|
48
|
+
const allowed = new Set(["scopes", "mode", "autoStart", "passiveBootstrap", "passiveTools", "logging", "showSuccessfulPatches", "historyLimit"]);
|
|
45
49
|
if (!isObject(value) || Object.keys(value).some((key) => !allowed.has(key))) {
|
|
46
50
|
throw new Error(`State Flow configuration contains unknown settings: ${path}`);
|
|
47
51
|
}
|
|
@@ -52,12 +56,21 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
|
|
|
52
56
|
if (Object.hasOwn(value, "logging") && typeof value.logging !== "boolean") throw new Error(`State Flow logging must be a boolean: ${path}`);
|
|
53
57
|
if (Object.hasOwn(value, "showSuccessfulPatches") && typeof value.showSuccessfulPatches !== "boolean") throw new Error(`State Flow showSuccessfulPatches must be a boolean: ${path}`);
|
|
54
58
|
if (Object.hasOwn(value, "historyLimit") && (!Number.isSafeInteger(value.historyLimit) || (value.historyLimit as number) < 0 || (value.historyLimit as number) > MAX_HISTORY_LIMIT)) throw new Error(`State Flow historyLimit must be an integer from 0 to ${MAX_HISTORY_LIMIT}: ${path}`);
|
|
59
|
+
let scopes = DEFAULT_SCOPE_SELECTION;
|
|
60
|
+
if (Object.hasOwn(value, "scopes")) {
|
|
61
|
+
try {
|
|
62
|
+
scopes = captureScopeSelection(value.scopes);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`Invalid State Flow scopes configuration: ${path}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
55
67
|
const legacyInactive: InactiveMode = value.passiveBootstrap !== false || value.passiveTools !== false ? "passive" : "off";
|
|
56
68
|
const hasLegacyMode = Object.hasOwn(value, "autoStart") || Object.hasOwn(value, "passiveBootstrap") || Object.hasOwn(value, "passiveTools");
|
|
57
69
|
const mode = isStateFlowMode(value.mode) ? value.mode : value.autoStart === true ? "active" : hasLegacyMode ? legacyInactive : "off";
|
|
58
70
|
return {
|
|
59
71
|
directory,
|
|
60
72
|
mode,
|
|
73
|
+
scopes,
|
|
61
74
|
inactiveMode: isStateFlowMode(value.mode) ? inactiveModeFor(value.mode) : hasLegacyMode ? legacyInactive : "off",
|
|
62
75
|
logging: value.logging === true,
|
|
63
76
|
showSuccessfulPatches: value.showSuccessfulPatches !== false,
|