@llblab/pi-kit 0.27.5 → 0.27.6

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/BACKLOG.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # Backlog
2
2
 
3
- The 0.27.5 composition is recorded in [CHANGELOG.md](./CHANGELOG.md). Package pins, resource order and bundled runtime ownership remain authoritative in `package.json`.
3
+ The 0.27.6 composition is recorded in [CHANGELOG.md](./CHANGELOG.md). Package pins, resource order and bundled runtime ownership remain authoritative in `package.json`.
4
+
5
+ ## Release acceptance
6
+
7
+ - **Publication:** Once State Flow 0.25.5 is verified on npm, install its exact pin, validate the packed kit and run the exact-tag workflow; verify GitHub Release and npm identity.
4
8
 
5
9
  ## Carried checks
6
10
 
11
+ - **Installed 0.27.6 restoration/compaction smoke (operator-owned):** After separately authorized installation/reload, use disposable storage to confirm failed Active restoration blocks provider inference across tree/reload, explicit mode recovery remains available, and native Codemode values/deletions survive completed-history compaction. Packed validation does not certify installed clients.
7
12
  - **Installed 0.27.5 cleanup smoke (operator-owned):** After separately authorized installation/reload, confirm State Flow missing-deletion hints, recursive empty-object cleanup, inherited fallback and preserved array slots in disposable storage. Packed validation does not certify installed clients.
8
13
  - **Installed 0.27.4 inspection smoke (operator-owned):** After separately authorized installation/reload, inspect a large nested State Flow field in disposable storage. Confirm readable JSON layout, separate truncation notices and intact genuine string escapes. Packed validation does not certify installed Telegram clients.
9
14
  - **Installed 0.27.3 single patch display smoke (operator-owned):** After separately authorized installation/reload, confirm one argument block followed by changed/no-op acknowledgements, compact configuration and visible rejected arguments/errors in disposable State Flow storage.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.27.6: State Flow Restoration Fence and Codemode Compaction
6
+
7
+ - `Restoration and Compaction`: Advances the exact State Flow pin to `0.25.5`. Failed Active restoration blocks inference before the provider rather than exposing native history; valid selection, accepted Start or explicit Passive/Off permits recovery. Native Codemode metadata no longer blocks completed-history compaction, while values/deletions survive compaction and resume. Unknown metadata and visible custom context stay protected. Other pins, resources and load order are unchanged.
8
+
5
9
  ## 0.27.5: State Flow Cleanup and Deletion Hints
6
10
 
7
11
  - `Cleanup and Deletion Hints`: Advances the exact State Flow pin to `0.25.4`. Accepted scopes recursively drop empty object fields without shifting array slots; cleanup may reveal inherited values. Missing authored deletions report their target and first unavailable component without blocking useful writes or creating revisions. Exact history and untouched scopes stay unchanged. Other pins, resources and load order are unchanged.
package/README.md CHANGED
@@ -17,7 +17,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
17
17
  | [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.3.0` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
18
18
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.12.1` | Shared Codex quota/Business credit status and persistent priority Fast toggle, mirrored in Telegram |
19
19
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
20
- | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.25.4` | Scoped context/memory compiler with intent-owned memory, recursive empty-object cleanup, missing-deletion hints and readable Telegram inspection |
20
+ | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.25.5` | Scoped context/memory compiler with intent-owned memory, failed Active restoration fencing and Codemode-safe compaction |
21
21
  | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.6` | Telegram companion with connection resume, Workspace slot recovery, follower Threads, filterable Skills, files, voice, and controls |
22
22
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
23
23
 
@@ -25,7 +25,7 @@ Versions are exact by design. An upstream release does not change an installed k
25
25
 
26
26
  ## Install
27
27
 
28
- Requires **Pi 1.0.0+** and **Node.js 22.19.0+**. State Flow requires its canonical checkpoint/tail storage format and does not convert unsupported stores in place. Preserve existing stores and consult the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.25.4/docs/usage.md#moving-a-store-and-supported-formats) before changing installations.
28
+ Requires **Pi 1.0.0+** and **Node.js 22.19.0+**. State Flow requires its canonical checkpoint/tail storage format and does not convert unsupported stores in place. Preserve existing stores and consult the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.25.5/docs/usage.md#moving-a-store-and-supported-formats) before changing installations.
29
29
 
30
30
  From npm:
31
31
 
@@ -45,7 +45,7 @@ Prefer the kit instead of separately loading the same packages. If you already u
45
45
 
46
46
  ## Development
47
47
 
48
- The `0.27.5` composition includes State Flow `0.25.4` with recursive empty-object cleanup and missing-deletion hints; all other pins, resources and load order remain unchanged. `npm run validate` checks exact installed pins, declared resources, dependency audit and bundled inventory. It does not certify installed-client rendering; carried checks remain in [Backlog](./BACKLOG.md).
48
+ The `0.27.6` composition includes State Flow `0.25.5` with failed Active restoration fencing and Codemode-safe compaction; all other pins, resources and load order remain unchanged. `npm run validate` checks exact installed pins, declared resources, dependency audit and bundled inventory. It does not certify installed-client rendering; carried checks remain in [Backlog](./BACKLOG.md).
49
49
 
50
50
  ```bash
51
51
  npm install
@@ -18,7 +18,7 @@ The [relocation ledger](docs/agent-contract-relocation.md) maps every pre-compac
18
18
 
19
19
  - Treat canonical scope files as semantic authority and Git only as optional backup. Classify complete, wholly absent, partial and malformed cohorts before recovery; absent shared pairs may initialize only under accepted authority, while incomplete/private evidence fails closed. Never repair through ad-hoc writes. See [storage recovery](docs/usage.md#storage-and-recovery) and [transaction rule](docs/filesystem-recovery.md#transaction-rule).
20
20
  - Keep exact regular-file, byte-CAS and lock-serialized publication with cancelable waits, single-use callback-scoped capabilities and guarded rollback. Never hold exclusion across inference, source acquisition or Git; do not steal interrupted locks, claim kernel-atomic multi-file publication or promise power-loss durability. See [asynchronous transaction](docs/architecture.md#asynchronous-storage-transaction) and [durability boundary](docs/filesystem-recovery.md#power-loss-durability).
21
- - Attach Off branches through native policy bookkeeping only, deferring memory acquisition and recovery diagnostics until explicit Passive/Active. Preserve pending Off forks across cold reload with child-owned native markers, without reading the parent header/store until acquisition. When memory is selected, restore private retained boundaries over live shared scopes and copy exact proven source-session history into a fresh fork owner; never substitute current, empty, Git or another branch on expiry/failure. Explicit Start instead validates current same-session authority. Only accepted candidates install cache, checkpoint and mode. See [Pi lifecycle](docs/architecture.md#pi-lifecycle) and [fork contract](docs/fork-contract.md).
21
+ - Attach Off branches through native policy bookkeeping only, deferring memory acquisition and recovery diagnostics until explicit Passive/Active. Preserve pending Off forks across cold reload with child-owned native markers, without reading the parent header/store until acquisition. When memory is selected, restore private retained boundaries over live shared scopes and copy exact proven source-session history into a fresh fork owner; never substitute current, empty, Git or another branch on expiry/failure. Explicit Start instead validates current same-session authority. Only accepted candidates install cache, checkpoint and mode. Failed selected Active restoration fences live inference through public Abort until valid selection, accepted Start or explicit Passive/Off; never silently expose native history as the inactive fallback. See [Pi lifecycle](docs/architecture.md#pi-lifecycle) and [fork contract](docs/fork-contract.md).
22
22
  - Persist each material semantic change and its affected revisions exactly once, including accepted Session responses. Optional settled-turn Git backup may capture only already-accepted owned files, leave unrelated index/worktree data intact and push without force or semantic side effects. Defer busy backup when the host provides no settlement operation signal rather than blocking Abort; never move it to `turn_end` or add a durable push queue. Off cancels owned captures/pushes and suppresses late reporting; normal agent-operation completion must not cancel an independently admitted push, and foreign callers' pushes retain ownership. See [optional Git backup](docs/architecture.md#optional-git-backup).
23
23
  - Accept one atomic `patch_state` cohort across supplied scopes against current shared memory; reject empty scopes, unknown/retired grammar and model-authored `response`. Correct no-ops create no transition. Enforce the single-call inference barrier before sibling tools execute and retain conservative model-facing reconciliation when a result cannot be predicted. See [model tools](docs/architecture.md#model-tools) and [Pi lifecycle](docs/architecture.md#pi-lifecycle).
24
24
  - Reconcile the actual accepted ordinary answer at `turn_end` with response-owned cancellation and one accepted lifecycle publication; do not request private repair inference, ceremonial finalization patches or roll back accepted memory after cancellation. See [Pi lifecycle](docs/architecture.md#pi-lifecycle).
@@ -1,6 +1,6 @@
1
1
  # Backlog
2
2
 
3
- The **0.25.4: Empty-Object Cleanup and Deletion Hints** hotfix outcomes belong in [CHANGELOG.md](CHANGELOG.md). This backlog retains publication, installed-client gates and deferred decisions.
3
+ The **0.25.5: Active Restoration Fence and Codemode Compaction** hotfix is prepared; outcomes belong in [CHANGELOG.md](CHANGELOG.md). This backlog retains publication, installed-client gates and deferred decisions.
4
4
 
5
5
  ## Out of scope
6
6
 
@@ -8,9 +8,13 @@ The **0.25.4: Empty-Object Cleanup and Deletion Hints** hotfix outcomes belong i
8
8
  - Nested `lazy_navigation` and intent-ownership warnings, validation or rejection. Authored missing-deletion hints do not change cascade behavior.
9
9
  - Any reduction of lifecycle state; the tagged-union question stays deferred.
10
10
 
11
+ ## 0.25.5 acceptance
12
+
13
+ - **Publication.** Run the exact-tag workflow and verify GitHub Release/npm identity, then synchronize and release Pi Kit 0.27.6.
14
+ - **Installed restoration/compaction smoke (operator-owned).** After separately authorized reload, use disposable storage to confirm failed Active restoration blocks provider inference across tree/reload, explicit mode recovery remains available, and native Codemode values/deletions survive completed-history compaction. Local/native tests do not certify the installed client.
15
+
11
16
  ## Carried gates
12
17
 
13
- - **0.25.4 publication and kit synchronization.** Verify the exact-tag workflow, GitHub Release and npm commit, then bundle and publish Pi Kit 0.27.5.
14
18
  - **Installed 0.25.4 cleanup smoke (operator-owned).** After separately authorized installation/reload, use disposable storage to confirm missing-deletion hints, recursive empty-object cleanup, inherited fallback and preserved array slots. Local validation and publication do not certify installed clients.
15
19
  - **Installed 0.25.3 inspection smoke (operator-owned).** After separately authorized installation/reload, inspect a large nested field: real layout newlines/quotes, separate omitted-character notice and intact genuine JSON string escapes. Use disposable storage; local adapter tests do not certify installed Telegram clients.
16
20
  - **Installed 0.25.2 smoke (operator-owned).** Disposable store: confirm one patch argument block followed by changed/no-op acknowledgements, compact rows with `showSuccessfulPatches: false`, and visible rejected arguments/errors.
@@ -2,7 +2,10 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
- ## Unreleased
5
+ ## 0.25.5: Active Restoration Fence and Codemode Compaction
6
+
7
+ - `Active restoration fence`: Failed retained Active restoration aborts live inference instead of silently exposing native history under an inactive fallback. Expired tree/reload and contradictory-lineage native tests prove zero provider calls and unchanged memory. Valid selection, accepted Start or explicit Passive/Off releases the fence; historical authority is never substituted.
8
+ - `Codemode-safe compaction`: Exact native `codemode-store` metadata no longer blocks completed-history compaction. Full branch records remain intact; native Codemode reads prove values and deletions survive compaction, reload and resume. Unknown metadata and visible custom context remain protected.
6
9
 
7
10
  ## 0.25.4: Empty-Object Cleanup and Deletion Hints
8
11
 
@@ -3,10 +3,11 @@ import { estimateTokens } from "@earendil-works/pi-coding-agent";
3
3
  export const STATE_FLOW_COMPACTION_SUMMARY = "State Flow accepted the completed work before this boundary. Current memory is restored from its retained semantic boundary and projected separately; use the retained native entries for subsequent work.";
4
4
  /** A modest margin above Pi's default 20k retained suffix absorbs estimation drift. */
5
5
  export const STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS = 24_000;
6
- function stateFlowEntry(entry) {
6
+ function contextInvisibleEntry(entry) {
7
+ // Codemode reconstructs its store from the full native branch, not the compacted model context.
7
8
  return entry.type === "custom"
8
9
  && typeof entry.customType === "string"
9
- && entry.customType.startsWith("state-flow-");
10
+ && (entry.customType.startsWith("state-flow-") || entry.customType === "codemode-store");
10
11
  }
11
12
  export function shouldRequestStateFlowCompaction(usage) {
12
13
  return typeof usage?.tokens === "number"
@@ -45,7 +46,7 @@ export function planStateFlowCompaction(entries, boundary, step, runAnchorTimest
45
46
  const keep = entries.findIndex(isRunAnchor);
46
47
  if (keep < 0 || keep > terminal || entries.findLastIndex(isRunAnchor) !== keep)
47
48
  return undefined;
48
- if (entries.slice(0, keep).some((entry) => entry.type === "custom_message" || (entry.type === "custom" && !stateFlowEntry(entry))))
49
+ if (entries.slice(0, keep).some((entry) => entry.type === "custom_message" || (entry.type === "custom" && !contextInvisibleEntry(entry))))
49
50
  return undefined;
50
51
  const firstKeptEntryId = entries[keep]?.id;
51
52
  const leafId = entries.at(-1)?.id;
@@ -42,7 +42,7 @@ export default function stateFlowExtension(pi, options = {}) {
42
42
  let scopeStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
43
43
  let effectiveState = {};
44
44
  let branchStartsWithoutRuntime = false;
45
- let selectedHistoryExpired = false;
45
+ let selectedBoundaryError;
46
46
  let modePersistenceError;
47
47
  /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
48
48
  const inactivePersistence = new OwnedOperationSlot();
@@ -390,7 +390,7 @@ export default function stateFlowExtension(pi, options = {}) {
390
390
  runtime = undefined;
391
391
  snapshot = emptySnapshot("off");
392
392
  branchStartsWithoutRuntime = withoutRuntime;
393
- selectedHistoryExpired = false;
393
+ selectedBoundaryError = undefined;
394
394
  modePersistenceError = persistenceError;
395
395
  forkInitialization = reason === "fork";
396
396
  installScopeStates();
@@ -408,7 +408,7 @@ export default function stateFlowExtension(pi, options = {}) {
408
408
  runtime = createRuntime(ctx);
409
409
  installScopeStates();
410
410
  branchStartsWithoutRuntime = false;
411
- selectedHistoryExpired = false;
411
+ selectedBoundaryError = undefined;
412
412
  forkInitialization = sessionStartReason === "fork";
413
413
  let selection = { kind: "settled" };
414
414
  let skipped = 0;
@@ -439,6 +439,8 @@ export default function stateFlowExtension(pi, options = {}) {
439
439
  ? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
440
440
  }
441
441
  catch (error) {
442
+ selectedBoundaryError = { expired: error instanceof HistoryBoundaryExpiredError,
443
+ blockInference: selected.checkpoint.mode === "active" && requestedMode === undefined };
442
444
  snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(selected.checkpoint.mode));
443
445
  }
444
446
  }
@@ -500,6 +502,13 @@ export default function stateFlowExtension(pi, options = {}) {
500
502
  throw new Error("State Flow branch selection changed while awaiting publication");
501
503
  };
502
504
  let accepted = false;
505
+ const failSelection = (error) => {
506
+ selectedBoundaryError = { expired: error instanceof HistoryBoundaryExpiredError,
507
+ blockInference: pending.requestedMode === undefined && (selection.kind === "auto-start"
508
+ || ((selection.kind === "restore" || selection.kind === "fork") && selection.checkpoint.mode === "active")) };
509
+ snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(snapshot.config.mode));
510
+ installScopeStates();
511
+ };
503
512
  // Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
504
513
  const accept = (candidate, next, nativeWrites) => {
505
514
  accepted = true;
@@ -583,10 +592,7 @@ export default function stateFlowExtension(pi, options = {}) {
583
592
  }
584
593
  if (!isCurrent())
585
594
  return;
586
- if (error instanceof HistoryBoundaryExpiredError)
587
- selectedHistoryExpired = true;
588
- snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
589
- installScopeStates();
595
+ failSelection(error);
590
596
  }
591
597
  finally {
592
598
  pending.awaitingAcceptance = false;
@@ -612,8 +618,7 @@ export default function stateFlowExtension(pi, options = {}) {
612
618
  // Host failures before acceptance leave the selection unavailable, never invented empty memory.
613
619
  if (accepted || !isCurrent())
614
620
  return;
615
- snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
616
- installScopeStates();
621
+ failSelection(error);
617
622
  }
618
623
  finally {
619
624
  pending.awaitingAcceptance = false;
@@ -642,7 +647,7 @@ export default function stateFlowExtension(pi, options = {}) {
642
647
  function settleSelection(ctx, reason, notifyRecovery, skipped, selectedMemory) {
643
648
  const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
644
649
  if (failure !== undefined && notifyRecovery && !modePersistenceError) {
645
- if (selectedHistoryExpired)
650
+ if (selectedBoundaryError?.expired)
646
651
  notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
647
652
  else
648
653
  notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
@@ -919,14 +924,14 @@ export default function stateFlowExtension(pi, options = {}) {
919
924
  ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
920
925
  const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
921
926
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
922
- const recoveredCurrent = selectedHistoryExpired;
927
+ const recoveredCurrent = selectedBoundaryError?.expired;
923
928
  publish(activated);
924
929
  accepted = true;
925
930
  inactivePersistence.cancel();
926
931
  runtime = selected = activation;
927
932
  snapshot = activated;
928
933
  activeContext = ctx;
929
- selectedHistoryExpired = false;
934
+ selectedBoundaryError = undefined;
930
935
  modePersistenceError = undefined;
931
936
  installScopeStates();
932
937
  const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
@@ -988,6 +993,8 @@ export default function stateFlowExtension(pi, options = {}) {
988
993
  /** Local policy, tools and UI change before any canonical wait. */
989
994
  function applyInactiveMode(ctx, mode) {
990
995
  snapshot = deactivateEpisode(snapshot, mode);
996
+ if (selectedBoundaryError)
997
+ selectedBoundaryError.blockInference = false;
991
998
  clearRunTransient();
992
999
  syncStateFlowTools();
993
1000
  updateUi(ctx);
@@ -1074,7 +1081,7 @@ export default function stateFlowExtension(pi, options = {}) {
1074
1081
  acquisition.reset();
1075
1082
  runtime = undefined;
1076
1083
  snapshot = emptySnapshot("off");
1077
- selectedHistoryExpired = false;
1084
+ selectedBoundaryError = undefined;
1078
1085
  branchStartsWithoutRuntime = preRuntime;
1079
1086
  forkInitialization = pendingFork;
1080
1087
  deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
@@ -1307,6 +1314,12 @@ export default function stateFlowExtension(pi, options = {}) {
1307
1314
  return { messages: projection.project(source, view, () => runtimeContextHead(snapshot, view, projectRecentTransitionsWithLimit(config.historyLimit, runtime?.recent() ?? []))) };
1308
1315
  }
1309
1316
  function prepareContext(messages, ctx) {
1317
+ // Pi continues after hook errors; public Abort fences inference instead of exposing the native history fallback.
1318
+ if (selectedBoundaryError?.blockInference && ctx.signal && !shuttingDown) {
1319
+ ctx.abort();
1320
+ notifyProblem(ctx, "State Flow inference blocked: Active memory restoration failed. Select a valid branch or use /state-flow-active for current memory; /state-flow-passive or /state-flow-off explicitly permits native context.", "error");
1321
+ return;
1322
+ }
1310
1323
  const pending = inferencePreparation.current;
1311
1324
  const selected = runtime;
1312
1325
  const operationSignal = ctx.signal;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.25.4",
3
+ "version": "0.25.5",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
@@ -328,7 +328,7 @@ The current user specification stays at user authority and appears only in synth
328
328
 
329
329
  **Why no retain-none boundaries.** State Flow does not use retain-none boundary compactions. Completed canonical state omits the exact user prompt, and foreign custom context can legitimately occur inside the latest retained iteration; hiding both would make the projected semantic state a lossy substitute for native context.
330
330
 
331
- **What prevents the boundary:** unknown or smaller usage, foreign custom metadata or native `custom_message` context in the prefix that would be removed, stale selection, Stop/bootstrap/error/abort, and pending input. User manual and native threshold/overflow compaction stay unmodified; unaccepted work stays under Pi's native compaction contract.
331
+ **What prevents the boundary:** unknown or smaller usage, foreign custom metadata or native `custom_message` context in the prefix that would be removed, stale selection, Stop/bootstrap/error/abort, and pending input. The exact native `custom` type `codemode-store` is exempt alongside State Flow metadata: it carries no model context, and Codemode reconstructs its values/deletions from the full append-only branch even after compaction. Unknown types, similarly named types and visible custom messages are not exempt. User manual and native threshold/overflow compaction stay unmodified; unaccepted work stays under Pi's native compaction contract.
332
332
 
333
333
  ### Context view and trajectory
334
334
 
@@ -510,6 +510,8 @@ Without removals on a complete accepted cohort, publication preserves semantic/p
510
510
  3. **Acceptance.** The selected boundary, the exact-source fork, a truly new auto-start origin (`withStartTransaction` with creation authority, rechecking branch evidence after waiting) or failed-Stop read-only recovery then use the awaited runtime API. Bootstrap and policy are derived after waiting and published in that single acceptance.
511
511
  4. **Installation.** Only the current lifetime installs memory, checkpoint, continuation, tools and UI before yielding; a later native-write failure only warns.
512
512
 
513
+ **Failed Active selection.** An unaccepted Active restoration/fork or automatic Start failure carries an inference fence independently of the inactive fallback mode. The live `context` hook calls public `ctx.abort()` before returning; merely throwing would let Pi continue with native history. Re-selecting/reloading the failed Active boundary reconstructs the fence. Accepted Start or a valid branch releases it; explicit Passive/Off permits native context but grants no missing historical/publication authority. Signal-less inspection does not cancel or clear the fence.
514
+
513
515
  **Interaction with mode choices:**
514
516
 
515
517
  - A new selection revokes older work; shutdown drains current and superseded restoration operations.
@@ -53,7 +53,7 @@ On the tested SDK, `before_agent_start` precedes the low-level agent's `prompt`;
53
53
  2. The active `context` hook supplies the operation signal and awaits preparation/maintenance before provider inference.
54
54
  3. If preparation fails, State Flow calls public `ctx.abort()`. Pi catches context-hook errors and may otherwise continue inference, so a thrown error alone is not a fence.
55
55
 
56
- Native tests prove no provider call before coherent acceptance, cancellation while an independent writer remains held, rollback without draft installation and preservation of uncompiled native input.
56
+ Native tests prove no provider call before coherent acceptance, cancellation while an independent writer remains held, rollback without draft installation and preservation of uncompiled native input. Failed retained Active restoration also aborts live inference before the provider; native witnesses cover expired tree selection/reload and contradictory private lineage without changing canonical bytes. Explicit inactive policy releases that inference fence, not the write fence.
57
57
 
58
58
  **Signals are not universal.** Idle commands and session events can lack them. Do not infer native Abort cancellation from an extension-owned shutdown signal or generalize active-run tests to idle waits.
59
59
 
@@ -126,7 +126,7 @@ This optional-backup policy does not apply to required semantic publication or r
126
126
  **State Flow-owned compaction:**
127
127
 
128
128
  - Requires known sufficient context usage and a proven retained run anchor.
129
- - Preserves the complete accepted run, skips protected foreign context and never requests retain-none shortening.
129
+ - Preserves the complete accepted run, skips protected foreign context and never requests retain-none shortening. Exact native `codemode-store` custom metadata permits shortening; native tests preserve its branch records and verify `load()` values/deletions through the real Codemode extension after compaction/reload/resume.
130
130
  - Leaves native manual/threshold/overflow compaction to Pi.
131
131
  - Awaits native compaction completion or refusal in the settled handler, so deferred companion prompts do not race it.
132
132
  - Skips compaction when usage is unknown or insufficient; a benign refusal permits a later attempt.
@@ -93,7 +93,7 @@ How each lifecycle event behaves:
93
93
  - A missing or ambiguous native anchor skips compaction instead of choosing the last steering message.
94
94
  - On resume, native `buildContextEntries()` and TUI rendering omit the older completed prefix.
95
95
  - Unknown or smaller usage skips the request, and custom Pi retention settings may still decline it benignly.
96
- - These prevent State Flow-owned shortening: foreign custom context in the prefix that would be removed, bootstrap/abort/error, Stop and pending input.
96
+ - These prevent State Flow-owned shortening: foreign custom context in the prefix that would be removed, bootstrap/abort/error, Stop and pending input. Pi's context-invisible `codemode-store` metadata does not block shortening: it stays on the full native branch, so `load()` and deletions survive compaction and resume. Unknown metadata and all `custom_message` entries remain protected.
97
97
  - Obsolete or inactive owned requests are canceled before their hook can fall through to a model summary, and a late completion cannot clear a newer request.
98
98
  - Ordinary manual/threshold/overflow compaction stays native and may preserve unfinished work not yet patched into memory.
99
99
 
@@ -281,6 +281,7 @@ A missing or expired private retained boundary is unavailable; State Flow does n
281
281
 
282
282
  **After a selected-boundary failure:**
283
283
 
284
+ - If the selected Active boundary cannot be restored, live inference is aborted before the provider instead of silently sending native history under an inactive fallback. Reload/resume retains this fence. Select a valid branch or explicitly Start from current same-session memory; explicit Passive/Off permits native context without repairing historical authority. Signal-less inspection remains observational.
284
285
  - Passive may still expose current global/CWD memory, but never the unavailable historical session layer or permission to publish an empty replacement.
285
286
  - Historical session reads and every `patch_state` refuse without changing canonical files or appending substitute checkpoints.
286
287
  - Passive/Off selection stays available and records the native policy/write fence described above. A later reload may expose validated current memory read-only, not the unavailable selected history.
@@ -26,10 +26,11 @@ type ActiveEntry = {
26
26
  message?: { role?: unknown; stopReason?: unknown; content?: unknown; timestamp?: unknown };
27
27
  };
28
28
 
29
- function stateFlowEntry(entry: ActiveEntry): boolean {
29
+ function contextInvisibleEntry(entry: ActiveEntry): boolean {
30
+ // Codemode reconstructs its store from the full native branch, not the compacted model context.
30
31
  return entry.type === "custom"
31
32
  && typeof entry.customType === "string"
32
- && entry.customType.startsWith("state-flow-");
33
+ && (entry.customType.startsWith("state-flow-") || entry.customType === "codemode-store");
33
34
  }
34
35
 
35
36
  export function shouldRequestStateFlowCompaction(usage: { tokens: number | null } | undefined): boolean {
@@ -70,7 +71,7 @@ export function planStateFlowCompaction(
70
71
  const isRunAnchor = (entry: ActiveEntry) => entry.type === "message" && entry.message?.role === "user" && entry.message.timestamp === runAnchorTimestamp;
71
72
  const keep = entries.findIndex(isRunAnchor);
72
73
  if (keep < 0 || keep > terminal || entries.findLastIndex(isRunAnchor) !== keep) return undefined;
73
- if (entries.slice(0, keep).some((entry) => entry.type === "custom_message" || (entry.type === "custom" && !stateFlowEntry(entry)))) return undefined;
74
+ if (entries.slice(0, keep).some((entry) => entry.type === "custom_message" || (entry.type === "custom" && !contextInvisibleEntry(entry)))) return undefined;
74
75
  const firstKeptEntryId = entries[keep]?.id;
75
76
  const leafId = entries.at(-1)?.id;
76
77
  if (typeof firstKeptEntryId !== "string" || typeof leafId !== "string") return undefined;
@@ -93,7 +93,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
93
93
  let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
94
94
  let effectiveState: SemanticState = {};
95
95
  let branchStartsWithoutRuntime = false;
96
- let selectedHistoryExpired = false;
96
+ let selectedBoundaryError: { expired: boolean; blockInference: boolean } | undefined;
97
97
  let modePersistenceError: string | undefined;
98
98
  /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
99
99
  const inactivePersistence = new OwnedOperationSlot<InactivePersistence>();
@@ -448,7 +448,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
448
448
  runtime = undefined;
449
449
  snapshot = emptySnapshot("off");
450
450
  branchStartsWithoutRuntime = withoutRuntime;
451
- selectedHistoryExpired = false;
451
+ selectedBoundaryError = undefined;
452
452
  modePersistenceError = persistenceError;
453
453
  forkInitialization = reason === "fork";
454
454
  installScopeStates();
@@ -464,7 +464,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
464
464
  runtime = createRuntime(ctx);
465
465
  installScopeStates();
466
466
  branchStartsWithoutRuntime = false;
467
- selectedHistoryExpired = false;
467
+ selectedBoundaryError = undefined;
468
468
  forkInitialization = sessionStartReason === "fork";
469
469
  let selection: BranchSelection = { kind: "settled" };
470
470
  let skipped = 0;
@@ -492,6 +492,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
492
492
  selection = { kind: "fork", source, checkpoint: sourceFence?.persistenceError !== undefined
493
493
  ? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
494
494
  } catch (error) {
495
+ selectedBoundaryError = { expired: error instanceof HistoryBoundaryExpiredError,
496
+ blockInference: selected.checkpoint.mode === "active" && requestedMode === undefined };
495
497
  snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(selected.checkpoint.mode));
496
498
  }
497
499
  } else if (selected.kind === "boundary") {
@@ -548,6 +550,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
548
550
  if (!isCurrent()) throw new Error("State Flow branch selection changed while awaiting publication");
549
551
  };
550
552
  let accepted = false;
553
+ const failSelection = (error: unknown): void => {
554
+ selectedBoundaryError = { expired: error instanceof HistoryBoundaryExpiredError,
555
+ blockInference: pending.requestedMode === undefined && (selection.kind === "auto-start"
556
+ || ((selection.kind === "restore" || selection.kind === "fork") && selection.checkpoint.mode === "active")) };
557
+ snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(snapshot.config.mode));
558
+ installScopeStates();
559
+ };
551
560
  // Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
552
561
  const accept = (candidate: TemporalRuntime, next: Snapshot, nativeWrites: () => void): void => {
553
562
  accepted = true;
@@ -616,9 +625,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
616
625
  return;
617
626
  }
618
627
  if (!isCurrent()) return;
619
- if (error instanceof HistoryBoundaryExpiredError) selectedHistoryExpired = true;
620
- snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
621
- installScopeStates();
628
+ failSelection(error);
622
629
  } finally {
623
630
  pending.awaitingAcceptance = false;
624
631
  }
@@ -637,8 +644,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
637
644
  } catch (error) {
638
645
  // Host failures before acceptance leave the selection unavailable, never invented empty memory.
639
646
  if (accepted || !isCurrent()) return;
640
- snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
641
- installScopeStates();
647
+ failSelection(error);
642
648
  } finally {
643
649
  pending.awaitingAcceptance = false;
644
650
  branchRestoration.release(pending);
@@ -668,7 +674,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
668
674
  function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
669
675
  const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
670
676
  if (failure !== undefined && notifyRecovery && !modePersistenceError) {
671
- if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
677
+ if (selectedBoundaryError?.expired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
672
678
  else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
673
679
  }
674
680
  const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
@@ -909,14 +915,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
909
915
  ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
910
916
  const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
911
917
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
912
- const recoveredCurrent = selectedHistoryExpired;
918
+ const recoveredCurrent = selectedBoundaryError?.expired;
913
919
  publish(activated);
914
920
  accepted = true;
915
921
  inactivePersistence.cancel();
916
922
  runtime = selected = activation;
917
923
  snapshot = activated;
918
924
  activeContext = ctx;
919
- selectedHistoryExpired = false;
925
+ selectedBoundaryError = undefined;
920
926
  modePersistenceError = undefined;
921
927
  installScopeStates();
922
928
  const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
@@ -984,6 +990,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
984
990
  /** Local policy, tools and UI change before any canonical wait. */
985
991
  function applyInactiveMode(ctx: ExtensionContext, mode: InactiveMode): void {
986
992
  snapshot = deactivateEpisode(snapshot, mode);
993
+ if (selectedBoundaryError) selectedBoundaryError.blockInference = false;
987
994
  clearRunTransient();
988
995
  syncStateFlowTools();
989
996
  updateUi(ctx);
@@ -1064,7 +1071,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1064
1071
  acquisition.reset();
1065
1072
  runtime = undefined;
1066
1073
  snapshot = emptySnapshot("off");
1067
- selectedHistoryExpired = false;
1074
+ selectedBoundaryError = undefined;
1068
1075
  branchStartsWithoutRuntime = preRuntime;
1069
1076
  forkInitialization = pendingFork;
1070
1077
  deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
@@ -1296,6 +1303,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1296
1303
  }
1297
1304
 
1298
1305
  function prepareContext(messages: AgentMessage[], ctx: ExtensionContext): ReturnType<typeof projectContext> | Promise<ReturnType<typeof projectContext>> {
1306
+ // Pi continues after hook errors; public Abort fences inference instead of exposing the native history fallback.
1307
+ if (selectedBoundaryError?.blockInference && ctx.signal && !shuttingDown) {
1308
+ ctx.abort();
1309
+ notifyProblem(ctx, "State Flow inference blocked: Active memory restoration failed. Select a valid branch or use /state-flow-active for current memory; /state-flow-passive or /state-flow-off explicitly permits native context.", "error");
1310
+ return;
1311
+ }
1299
1312
  const pending = inferencePreparation.current;
1300
1313
  const selected = runtime;
1301
1314
  const operationSignal = ctx.signal;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.25.4",
3
+ "version": "0.25.5",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.27.5",
3
+ "version": "0.27.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -46,7 +46,7 @@
46
46
  "@llblab/pi-clean-room": "0.3.0",
47
47
  "@llblab/pi-codex-usage": "0.12.1",
48
48
  "@llblab/pi-grow-loop": "0.9.0",
49
- "@llblab/pi-state-flow": "0.25.4",
49
+ "@llblab/pi-state-flow": "0.25.5",
50
50
  "@llblab/pi-telegram": "0.51.6",
51
51
  "@llblab/skills": "1.15.0"
52
52
  },