@llblab/pi-kit 0.27.3 → 0.27.5
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 +3 -1
- package/CHANGELOG.md +8 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +9 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +52 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +15 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +15 -5
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +4 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +1 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +61 -11
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +15 -18
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +17 -5
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +1 -1
- package/package.json +2 -2
package/BACKLOG.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Backlog
|
|
2
2
|
|
|
3
|
-
The 0.27.
|
|
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`.
|
|
4
4
|
|
|
5
5
|
## Carried checks
|
|
6
6
|
|
|
7
|
+
- **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
|
+
- **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.
|
|
7
9
|
- **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.
|
|
8
10
|
- **Installed 0.27.2 cascade receipt smoke (operator-owned):** After separately authorized installation/reload, close an intent owning a nested lazy key in disposable State Flow storage and confirm the receipt lists its owner path under `cascaded`, without the body. Packed validation does not certify installed clients.
|
|
9
11
|
- **Installed 0.27.1 Fast status smoke (operator-owned):** After separately authorized installation/reload, check Telegram menu Fast on/off and model switching for Codex and Claude Opus; unsupported Claude families must not show Fast. Use disposable settings and mocked quota where possible; do not mutate live stores or reconnect Telegram without separate authorization.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.27.5: State Flow Cleanup and Deletion Hints
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
|
|
9
|
+
## 0.27.4: Readable State Flow Inspection
|
|
10
|
+
|
|
11
|
+
- `Readable Inspection`: Advances the exact State Flow pin to published `0.25.3`. Large Telegram inspection fields now show pretty-printed JSON prefixes directly, with separate truncation notices instead of escaped `preview` strings. Genuine JSON string escapes and storage semantics are preserved. Other pins, resources and load order are unchanged.
|
|
12
|
+
|
|
5
13
|
## 0.27.3: State Flow Single Patch Display
|
|
6
14
|
|
|
7
15
|
- `Single Patch Display`: Advances the exact State Flow pin to published `0.25.2`. Interactive patch rows show JSON arguments once, followed by the actual acceptance, no-op or error acknowledgement. Compact configuration still hides successful arguments. Other pins, resources, load order and storage semantics 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.
|
|
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 |
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
npm install
|
|
@@ -8,7 +8,7 @@ The [relocation ledger](docs/agent-contract-relocation.md) maps every pre-compac
|
|
|
8
8
|
- Keep `index.ts` minimal, independent domains in `lib/` with same-named tests, architecture checks in `tests/invariants.test.ts`, and Pi lifecycle composition in `lib/extension.ts`; delegate low-level mechanics to their owners. See [composition](docs/architecture.md#composition).
|
|
9
9
|
- Keep Off the genuinely new-session default, with Passive and Active opt-in; Off removes State Flow model tools/context without deleting memory. Modes belong to the current session; global mode is only a new-session default. Passive declares both tools even without memory, but contributes protocol/projected state only with a validated view. See [mode behavior](docs/usage.md#active-passive-and-configured-off) and [configuration](docs/usage.md#configuration).
|
|
10
10
|
- Preserve Pi's native tool loop, trace and foreign context; the context domain alone projects the raw scope overlay. Freeze/rebase the head only at specified boundaries, retain current-run trajectory and stable-position tail notices, and never give projection IDs or guessed user anchors publication/compaction authority. See [Pi lifecycle](docs/architecture.md#pi-lifecycle) and [projection evidence](docs/performance.md#context-projection-and-trajectory-selection).
|
|
11
|
-
- Store only present known `intents`, `contract`, `working`, `artifacts`, `response`, `lazy` fields; preserve nested data and retained causal identities. Missing planes and empty response do not become fabricated stored defaults; malformed evidence fails closed. Keep lazy bodies out of automatic projection and Session as the sole author of new responses. See [semantic state](docs/architecture.md#semantic-state) and [temporal model](docs/architecture.md#temporal-model).
|
|
11
|
+
- Store only present known `intents`, `contract`, `working`, `artifacts`, `response`, `lazy` fields; preserve nonempty nested data and retained causal identities. Normalize empty object fields/planes only in supplied scopes and response-owned Session, before ownership and after cascades/compilation; preserve array slots and exact current/historical reads. Model projection may omit legacy empty branches. Missing planes and empty response do not become fabricated stored defaults; malformed evidence fails closed. Keep lazy bodies out of automatic projection and Session as the sole author of new responses. See [semantic state](docs/architecture.md#semantic-state) and [temporal model](docs/architecture.md#temporal-model).
|
|
12
12
|
- Respect global → CWD → session ownership, scope-local deletion and inherited fallbacks; scope does not confer instruction authority. Registered Skill ownership follows Pi source provenance, not path shape. See [semantic state](docs/architecture.md#semantic-state) and [artifact routing](docs/architecture.md#artifact-routing).
|
|
13
13
|
- Artifacts name exact registered source paths; observe only regular non-symlink files, never discover directories or read unrelated bodies. Preserve hidden per-scope provenance, exact-owner compilation, stable fingerprint checks and separate Skill hashes; unavailable sources are not proof of deletion. See [artifact routing](docs/architecture.md#artifact-routing).
|
|
14
14
|
- Preserve session config/runtime, per-scope metadata, lineage and provenance outside model-patchable state; decode legacy mode evidence read-only, never mix representations or normalize storage eagerly. See [storage and identity](docs/architecture.md#storage-and-identity) and [mode compatibility](docs/compatibility.md#mode-configuration-compatibility).
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Backlog
|
|
2
2
|
|
|
3
|
-
The **0.25.
|
|
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.
|
|
4
4
|
|
|
5
5
|
## Out of scope
|
|
6
6
|
|
|
7
7
|
- Changing what a cascade deletes, including writes into an owned target made by the closing patch. That stays deleted by design and already appears in the receipt.
|
|
8
|
-
- Nested `lazy_navigation
|
|
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
11
|
## Carried gates
|
|
12
12
|
|
|
13
|
-
- **0.25.
|
|
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
|
+
- **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
|
+
- **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.
|
|
14
16
|
- **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.
|
|
15
17
|
- **Installed 0.25.1 smoke (operator-owned).** Disposable store: close an intent that owns a nested lazy key and confirm the receipt lists it under `cascaded`. May be combined with the open 0.25.0 smoke.
|
|
16
18
|
- **Installed 0.22.0, 0.23.0, 0.24.0 and 0.25.0 smokes** remain open as recorded.
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.25.4: Empty-Object Cleanup and Deletion Hints
|
|
8
|
+
|
|
9
|
+
- `Safe missing deletions`: Deletion-only patches through absent ancestors succeed without creating data or blocking other writes. Missing authored targets report their owner-scoped JSON Pointer and first unavailable component; hints remain outside state/history and create no revisions. Mixed new objects process `null` as deletion markers. Stored null and array-index deletion remain rejected; ordinary repeats and intent cascades stay quiet.
|
|
10
|
+
- `Recursive empty-object cleanup`: Accepted supplied scopes and response-owned Session omit empty object fields/planes, before ownership and after cascades/compilation. Explicit `{}` carries no retained value; roots, array slots and empty arrays survive. Model context hides legacy empty branches; untouched scopes and exact historical reads/replay remain unchanged. Cleanup can reveal inherited values and is recorded once as ordinary deletions.
|
|
11
|
+
|
|
12
|
+
## 0.25.3: Readable Telegram Inspection
|
|
13
|
+
|
|
14
|
+
- `Readable inspection`: Telegram memory inspection shows large fields as direct pretty-printed JSON prefixes instead of JSON-encoded `preview` strings with escaped layout and quotes. A separate notice reports omitted characters. Transport-aware budgeting preserves the Rich message ceiling and Unicode boundaries; genuine JSON string escapes, stored state and exact `read_state` results remain unchanged.
|
|
15
|
+
|
|
5
16
|
## 0.25.2: Single Patch Display
|
|
6
17
|
|
|
7
18
|
- `Single patch display`: Interactive `patch_state` rows show JSON arguments once in the call, followed by the actual acceptance, no-op or error acknowledgement instead of a second patch. `showSuccessfulPatches: false` hides call arguments while retaining compact acknowledgements; rejected arguments remain visible. Atomic publication, stored state and model-facing `state_updates` receipts are unchanged.
|
|
@@ -145,6 +145,7 @@ Registered Pi Skills may be compiled into source-addressed artifacts when durabl
|
|
|
145
145
|
- Overlapping assignments follow successful acceptance order. Correct repeats succeed without new semantic revisions.
|
|
146
146
|
- Session remains private.
|
|
147
147
|
- Object patches merge recursively, and `null` deletes an object key instead of being stored.
|
|
148
|
+
- Accepted supplied scopes recursively drop empty object fields and planes, including explicit `{}`; empty ancestors disappear. Array slots and empty arrays stay intact. Cleanup is scope-local and may reveal inherited values. Model context hides legacy empty branches, but reading never rewrites history. See [patch semantics](docs/architecture.md#model-tools).
|
|
148
149
|
|
|
149
150
|
```json
|
|
150
151
|
{
|
|
@@ -43,9 +43,12 @@ function lazyValueKind(value) {
|
|
|
43
43
|
return "object";
|
|
44
44
|
return typeof value;
|
|
45
45
|
}
|
|
46
|
+
function hasLazyContent(value) {
|
|
47
|
+
return !isObject(value) || Object.values(value).some(hasLazyContent);
|
|
48
|
+
}
|
|
46
49
|
/** Fixed-budget navigation only: never place lazy bodies or partial key catalogs in baseline context. */
|
|
47
50
|
export function lazyNavigationHint(state) {
|
|
48
|
-
const entries = isObject(state.lazy) ? Object.entries(state.lazy) : [];
|
|
51
|
+
const entries = isObject(state.lazy) ? Object.entries(state.lazy).filter(([, value]) => hasLazyContent(value)) : [];
|
|
49
52
|
const base = { available: entries.length > 0, path: LAZY_HINT_PATH };
|
|
50
53
|
if (!base.available)
|
|
51
54
|
return base;
|
|
@@ -196,6 +199,8 @@ export class ContextProjection {
|
|
|
196
199
|
}
|
|
197
200
|
const prefix = (a, b) => a.length <= b.length && a.every((part, index) => part === b[index]);
|
|
198
201
|
updates.effective = updates.effective.filter((entry) => {
|
|
202
|
+
if ("deleted" in entry && this.view && known(entry.path) === undefined)
|
|
203
|
+
return false;
|
|
199
204
|
const matches = ({ path }) => path.length === entry.path.length && prefix(path, entry.path);
|
|
200
205
|
const authored = leaves.findLast(matches) ?? objects.findLast(matches);
|
|
201
206
|
if (!authored)
|
|
@@ -808,9 +808,9 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
808
808
|
? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
|
|
809
809
|
: "\nState already current.";
|
|
810
810
|
return { content: [
|
|
811
|
-
{ type: "text", text: acknowledgement },
|
|
811
|
+
{ type: "text", text: acknowledgement + stage.missingDeletions.map(({ path, unavailablePath }) => `\n\nHint: deletion skipped at ${path}; target absent. First unavailable component: ${unavailablePath}. Check the path if you expected an existing value.`).join("") },
|
|
812
812
|
...(updates ? [{ type: "text", text: `\n${presentationJson({ state_updates: updates })}` }] : []),
|
|
813
|
-
], details: { scopes, step: snapshot.meta.step, changed } };
|
|
813
|
+
], details: { scopes, step: snapshot.meta.step, changed, missingDeletions: stage.missingDeletions } };
|
|
814
814
|
}, signal);
|
|
815
815
|
}
|
|
816
816
|
catch (error) {
|
|
@@ -2,8 +2,16 @@ export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObj
|
|
|
2
2
|
export interface JsonObject {
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
}
|
|
5
|
+
export interface MissingDeletion {
|
|
6
|
+
/** JSON Pointer paths relative to the patched object. */
|
|
7
|
+
path: string;
|
|
8
|
+
unavailablePath: string;
|
|
9
|
+
}
|
|
10
|
+
/** Remove empty object fields without removing array slots or mutating the input. */
|
|
11
|
+
export declare function pruneEmptyObjects(value: JsonObject): JsonObject;
|
|
12
|
+
export declare function pruneEmptyObjects(value: JsonValue): JsonValue;
|
|
5
13
|
/** Detach at the mutable public boundary; share untouched paths only inside the owned draft. */
|
|
6
|
-
export declare function applyPatch(state: JsonObject, patch: JsonObject): JsonObject;
|
|
14
|
+
export declare function applyPatch(state: JsonObject, patch: JsonObject, missingDeletions?: MissingDeletion[]): JsonObject;
|
|
7
15
|
export declare function isObject(value: JsonValue | unknown): value is JsonObject;
|
|
8
16
|
export declare function validatePatch(value: unknown): asserts value is JsonObject;
|
|
9
17
|
export declare function canonicalJson(value: JsonValue | unknown): string;
|
|
@@ -4,17 +4,17 @@ function isIndexedArrayPatch(value) {
|
|
|
4
4
|
const keys = Object.keys(value);
|
|
5
5
|
return keys.length > 0 && keys.every((key) => ARRAY_INDEX_SELECTOR.test(key));
|
|
6
6
|
}
|
|
7
|
-
function applyOwnedValue(current, value, owned) {
|
|
7
|
+
function applyOwnedValue(current, value, owned, missingDeletions, path, unavailablePath) {
|
|
8
8
|
// Preserve inherited-object merge semantics without borrowing prototype objects.
|
|
9
9
|
if (!owned && current !== null && typeof current === "object")
|
|
10
10
|
current = structuredClone(current);
|
|
11
11
|
return Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
|
|
12
|
-
? applyOwnedArrayPatch(current, value)
|
|
13
|
-
: isObject(
|
|
14
|
-
? applyOwnedPatch(current, value)
|
|
12
|
+
? applyOwnedArrayPatch(current, value, missingDeletions, path, unavailablePath)
|
|
13
|
+
: isObject(value)
|
|
14
|
+
? applyOwnedPatch(isObject(current) ? current : {}, value, missingDeletions, path, unavailablePath ?? (!isObject(current) ? path : undefined))
|
|
15
15
|
: structuredClone(value);
|
|
16
16
|
}
|
|
17
|
-
function applyOwnedArrayPatch(state, patch) {
|
|
17
|
+
function applyOwnedArrayPatch(state, patch, missingDeletions, path, unavailablePath) {
|
|
18
18
|
let next = state;
|
|
19
19
|
for (const [selector, value] of Object.entries(patch)) {
|
|
20
20
|
const index = Number(ARRAY_INDEX_SELECTOR.exec(selector)[1]);
|
|
@@ -25,7 +25,7 @@ function applyOwnedArrayPatch(state, patch) {
|
|
|
25
25
|
throw new Error(`State patch array index ${selector} cannot be deleted; replace the whole array instead`);
|
|
26
26
|
const owns = Object.hasOwn(next, index);
|
|
27
27
|
const current = next[index];
|
|
28
|
-
const materialized = applyOwnedValue(current, value, owns);
|
|
28
|
+
const materialized = applyOwnedValue(current, value, owns, missingDeletions, `${path}/${index}`, unavailablePath);
|
|
29
29
|
if (owns && Object.is(current, materialized))
|
|
30
30
|
continue;
|
|
31
31
|
if (next === state)
|
|
@@ -34,20 +34,27 @@ function applyOwnedArrayPatch(state, patch) {
|
|
|
34
34
|
}
|
|
35
35
|
return next;
|
|
36
36
|
}
|
|
37
|
-
function applyOwnedPatch(state, patch) {
|
|
37
|
+
function applyOwnedPatch(state, patch, missingDeletions, path = "", unavailablePath) {
|
|
38
38
|
let next = state;
|
|
39
39
|
for (const [key, value] of Object.entries(patch)) {
|
|
40
40
|
const owns = Object.hasOwn(next, key);
|
|
41
|
+
const target = missingDeletions ? `${path}/${key.replace(/~/g, "~0").replace(/\//g, "~1")}` : "";
|
|
41
42
|
if (value === null) {
|
|
42
|
-
if (!owns)
|
|
43
|
+
if (!owns) {
|
|
44
|
+
missingDeletions?.push({ path: target, unavailablePath: unavailablePath ?? target });
|
|
43
45
|
continue;
|
|
46
|
+
}
|
|
44
47
|
if (next === state)
|
|
45
48
|
next = { ...state };
|
|
46
49
|
delete next[key];
|
|
47
50
|
continue;
|
|
48
51
|
}
|
|
49
52
|
const current = next[key];
|
|
50
|
-
const materialized = applyOwnedValue(current, value, owns);
|
|
53
|
+
const materialized = applyOwnedValue(current, value, owns, missingDeletions, target, unavailablePath ?? (!owns ? target : undefined));
|
|
54
|
+
// Deletion-only patches must not fabricate absent ancestor objects; explicit {} still writes.
|
|
55
|
+
if (!owns && current === undefined && isObject(value) && Object.keys(value).length > 0
|
|
56
|
+
&& isObject(materialized) && Object.keys(materialized).length === 0)
|
|
57
|
+
continue;
|
|
51
58
|
if (owns && Object.is(current, materialized))
|
|
52
59
|
continue;
|
|
53
60
|
if (next === state)
|
|
@@ -56,9 +63,43 @@ function applyOwnedPatch(state, patch) {
|
|
|
56
63
|
}
|
|
57
64
|
return next;
|
|
58
65
|
}
|
|
66
|
+
export function pruneEmptyObjects(value) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
let next = value;
|
|
69
|
+
for (let index = 0; index < value.length; index++) {
|
|
70
|
+
const pruned = pruneEmptyObjects(value[index]);
|
|
71
|
+
if (pruned === value[index])
|
|
72
|
+
continue;
|
|
73
|
+
if (next === value)
|
|
74
|
+
next = value.slice();
|
|
75
|
+
next[index] = pruned;
|
|
76
|
+
}
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
if (!isObject(value))
|
|
80
|
+
return value;
|
|
81
|
+
let next = value;
|
|
82
|
+
for (const [key, child] of Object.entries(value)) {
|
|
83
|
+
const pruned = pruneEmptyObjects(child);
|
|
84
|
+
const empty = isObject(pruned) && Object.keys(pruned).length === 0;
|
|
85
|
+
if (!empty && pruned === child)
|
|
86
|
+
continue;
|
|
87
|
+
if (next === value)
|
|
88
|
+
next = { ...value };
|
|
89
|
+
if (empty)
|
|
90
|
+
delete next[key];
|
|
91
|
+
else
|
|
92
|
+
Object.defineProperty(next, key, { value: pruned, enumerable: true, configurable: true, writable: true });
|
|
93
|
+
}
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
59
96
|
/** Detach at the mutable public boundary; share untouched paths only inside the owned draft. */
|
|
60
|
-
export function applyPatch(state, patch) {
|
|
61
|
-
|
|
97
|
+
export function applyPatch(state, patch, missingDeletions) {
|
|
98
|
+
const collected = missingDeletions ? [] : undefined;
|
|
99
|
+
const next = applyOwnedPatch(structuredClone(state), patch, collected);
|
|
100
|
+
if (collected)
|
|
101
|
+
missingDeletions.push(...collected);
|
|
102
|
+
return next;
|
|
62
103
|
}
|
|
63
104
|
export function isObject(value) {
|
|
64
105
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT } from "./history.js";
|
|
2
2
|
import { isObject, sameJson } from "./json.js";
|
|
3
|
-
import { emptyState, projectSemanticPatch,
|
|
3
|
+
import { emptyState, projectSemanticPatch, projectStateForRead } from "./state.js";
|
|
4
4
|
import { readTemporalView } from "./temporal.js";
|
|
5
5
|
const MAX_REFERENCE_SOURCES = 3;
|
|
6
6
|
const MAX_REFERENCE_SCAN_NODES = 10_000;
|
|
@@ -32,7 +32,7 @@ export function readStatePath(view, path, historyLimit = DEFAULT_HISTORY_LIMIT)
|
|
|
32
32
|
const boundary = view.lineage[view.lineage.length - 1 - query.offset];
|
|
33
33
|
if (!boundary)
|
|
34
34
|
throw new Error("Requested history predates the proven temporal origin");
|
|
35
|
-
return { path, boundary: structuredClone(boundary), state:
|
|
35
|
+
return { path, boundary: structuredClone(boundary), state: projectStateForRead(readTemporalView(view, query.offset, query.scope, historyLimit)) };
|
|
36
36
|
}
|
|
37
37
|
const record = view.scopes[query.scope].patches.at(-1 - query.offset);
|
|
38
38
|
if (!record)
|
|
@@ -271,7 +271,7 @@ export function readProjectedState(view, paths, projection = "value", historyLim
|
|
|
271
271
|
throw new Error("Value and keys projections require a state path");
|
|
272
272
|
const readsLazy = selectors[0]?.kind === "key" && selectors[0].key === "lazy";
|
|
273
273
|
const raw = readTemporalView(view, query.offset, query.scope, historyLimit);
|
|
274
|
-
const state = readsLazy ? raw :
|
|
274
|
+
const state = readsLazy ? raw : projectStateForRead(raw);
|
|
275
275
|
const field = selectors.length === 1 && selectors[0]?.kind === "key" ? selectors[0].key : undefined;
|
|
276
276
|
if (projection === "value" && field !== undefined && Object.hasOwn(emptyState(), field) && !Object.hasOwn(state, field))
|
|
277
277
|
return { value: null };
|
|
@@ -83,5 +83,7 @@ export declare function selectSemanticFields(value: JsonObject): JsonObject;
|
|
|
83
83
|
export declare function projectSemanticState(state: JsonObject): SemanticState;
|
|
84
84
|
/** Preserve deletion meaning in visible history without exposing ignored fields or empty responses. */
|
|
85
85
|
export declare function projectSemanticPatch(patch: JsonObject): JsonObject;
|
|
86
|
-
/**
|
|
86
|
+
/** Explicit hot reads filter hidden bodies/bookkeeping without normalizing legacy semantic data. */
|
|
87
|
+
export declare function projectStateForRead(state: SemanticState, artifactHints?: ArtifactModelHints): SemanticState;
|
|
88
|
+
/** Ordinary model context omits empty object fields; exact reads and replay remain observational. */
|
|
87
89
|
export declare function projectStateForModel(state: SemanticState, artifactHints?: ArtifactModelHints): SemanticState;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isArtifactRegistry, projectArtifactsForModel, updateArtifactRegistry, } from "./artifact.js";
|
|
2
|
-
import { applyPatch, isObject } from "./json.js";
|
|
2
|
+
import { applyPatch, isObject, pruneEmptyObjects } from "./json.js";
|
|
3
3
|
export function emptyState() {
|
|
4
4
|
return { intents: {}, contract: {}, working: {}, artifacts: {}, response: "", lazy: {} };
|
|
5
5
|
}
|
|
@@ -50,11 +50,15 @@ export function projectSemanticPatch(patch) {
|
|
|
50
50
|
projected.response = null;
|
|
51
51
|
return projected;
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
export function
|
|
53
|
+
/** Explicit hot reads filter hidden bodies/bookkeeping without normalizing legacy semantic data. */
|
|
54
|
+
export function projectStateForRead(state, artifactHints = {}) {
|
|
55
55
|
const { lazy: _hidden, ...visible } = state;
|
|
56
56
|
const projected = projectSemanticState(visible);
|
|
57
57
|
if (projected.artifacts)
|
|
58
58
|
projected.artifacts = projectArtifactsForModel(projected.artifacts, artifactHints);
|
|
59
59
|
return projected;
|
|
60
60
|
}
|
|
61
|
+
/** Ordinary model context omits empty object fields; exact reads and replay remain observational. */
|
|
62
|
+
export function projectStateForModel(state, artifactHints = {}) {
|
|
63
|
+
return pruneEmptyObjects(projectStateForRead(state, artifactHints));
|
|
64
|
+
}
|
|
@@ -73,35 +73,32 @@ const STATE_FLOW_SCOPE_LABELS = {
|
|
|
73
73
|
session: "💬 Session",
|
|
74
74
|
effective: "🧬 Effective",
|
|
75
75
|
};
|
|
76
|
-
//
|
|
77
|
-
//
|
|
76
|
+
// Budget the transport-serialized text, leaving room for six fields and notices
|
|
77
|
+
// below Telegram's 32,768-character Rich message ceiling.
|
|
78
78
|
const STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS = 3_000;
|
|
79
79
|
function renderStateFlowTelegramField(value) {
|
|
80
80
|
const json = JSON.stringify(value, null, 2);
|
|
81
|
-
if (json.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS)
|
|
82
|
-
return json;
|
|
81
|
+
if (JSON.stringify(json).length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
82
|
+
return [{ type: "pre", language: "json", text: json }];
|
|
83
|
+
}
|
|
83
84
|
let low = 0;
|
|
84
|
-
let high = json.length;
|
|
85
|
-
let rendered = "";
|
|
85
|
+
let high = Math.min(json.length, STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS);
|
|
86
86
|
while (low <= high) {
|
|
87
87
|
const length = Math.floor((low + high) / 2);
|
|
88
|
-
|
|
89
|
-
truncated: true,
|
|
90
|
-
...(value !== null && typeof value === "object" && !Array.isArray(value)
|
|
91
|
-
? { keys: Object.keys(value) }
|
|
92
|
-
: {}),
|
|
93
|
-
preview: json.slice(0, length),
|
|
94
|
-
omittedChars: json.length - length,
|
|
95
|
-
}, null, 2);
|
|
96
|
-
if (candidate.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
97
|
-
rendered = candidate;
|
|
88
|
+
if (JSON.stringify(json.slice(0, length)).length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
98
89
|
low = length + 1;
|
|
99
90
|
}
|
|
100
91
|
else {
|
|
101
92
|
high = length - 1;
|
|
102
93
|
}
|
|
103
94
|
}
|
|
104
|
-
|
|
95
|
+
// Do not split a Unicode surrogate pair at the preview boundary.
|
|
96
|
+
if (/[\uD800-\uDBFF]/.test(json.charAt(high - 1)))
|
|
97
|
+
high -= 1;
|
|
98
|
+
return [
|
|
99
|
+
{ type: "pre", language: "json", text: json.slice(0, high) },
|
|
100
|
+
{ type: "pre", text: `Truncated preview — ${json.length - high} characters omitted.` },
|
|
101
|
+
];
|
|
105
102
|
}
|
|
106
103
|
export function renderStateFlowRichState(scope, revisions, state) {
|
|
107
104
|
const fields = scope === "global" || scope === "cwd"
|
|
@@ -120,7 +117,7 @@ export function renderStateFlowRichState(scope, revisions, state) {
|
|
|
120
117
|
...fields.filter((field) => state[field] !== undefined && state[field] !== "").map((field) => ({
|
|
121
118
|
type: "details",
|
|
122
119
|
summary: { type: "code", text: field },
|
|
123
|
-
blocks:
|
|
120
|
+
blocks: renderStateFlowTelegramField(state[field]),
|
|
124
121
|
})),
|
|
125
122
|
],
|
|
126
123
|
skip_entity_detection: true,
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type ArtifactProvenance } from "./artifact.ts";
|
|
2
2
|
import type { SuccessfulArtifactRead } from "./acquisition.ts";
|
|
3
3
|
import { type AcceptedTransition } from "./history.ts";
|
|
4
|
+
import { type MissingDeletion } from "./json.ts";
|
|
4
5
|
import { type OwnedPath } from "./ownership.ts";
|
|
5
6
|
import { type SuccessfulSkillRead } from "./skills.ts";
|
|
6
7
|
import type { Snapshot } from "./snapshot.ts";
|
|
7
8
|
import type { AtomicScopePatches, ScopedSemanticStates, StateScope, TerminalTransition } from "./state.ts";
|
|
8
9
|
export interface StagedScopedTransition {
|
|
9
10
|
nextStates: ScopedSemanticStates;
|
|
11
|
+
/** Authored no-op deletions; presentation evidence only, never replay input. */
|
|
12
|
+
missingDeletions: MissingDeletion[];
|
|
10
13
|
/** Scope-local targets removed by the staged intent cascade, not replay input. */
|
|
11
14
|
cascades: Record<StateScope, OwnedPath[]>;
|
|
12
15
|
stateHashes: Record<StateScope, string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compileArtifact, ORDINARY_ARTIFACT_COMPILER, validateArtifactMetadata, validateArtifactRegistry, validateModelArtifactPatch, } from "./artifact.js";
|
|
2
2
|
import { createAcceptedTransition } from "./history.js";
|
|
3
|
-
import { applyPatch, containsNull, hashJson, isObject, validatePatch } from "./json.js";
|
|
3
|
+
import { applyPatch, containsNull, hashJson, isObject, pruneEmptyObjects, validatePatch } from "./json.js";
|
|
4
4
|
import { cascadeDeletionPatch, computeIntentCascade } from "./ownership.js";
|
|
5
5
|
import { hasCompiledSkillArtifact, SKILL_ARTIFACT_COMPILER } from "./skills.js";
|
|
6
6
|
import { emptyState } from "./state.js";
|
|
@@ -137,27 +137,37 @@ function stageScopedSemanticTransition(currentStates, transition, successfulSkil
|
|
|
137
137
|
const nextStates = { ...currentStates };
|
|
138
138
|
const provenanceUpdates = { global: {}, cwd: {}, session: {} };
|
|
139
139
|
const cascades = { global: [], cwd: [], session: [] };
|
|
140
|
+
const missingDeletions = [];
|
|
140
141
|
for (const scope of SCOPES) {
|
|
141
142
|
const authored = patches.get(scope) ?? {};
|
|
142
143
|
const patch = { ...authored, ...(scope === "session" && acceptedResponse !== undefined ? { response: acceptedResponse } : {}) };
|
|
143
|
-
|
|
144
|
+
const scopedMissing = [];
|
|
145
|
+
const normalize = patches.has(scope) || scope === "session" && acceptedResponse !== undefined;
|
|
146
|
+
const applied = applyPatch(currentStates[scope], patch, scopedMissing);
|
|
147
|
+
const patched = normalize ? pruneEmptyObjects(applied) : applied;
|
|
148
|
+
missingDeletions.push(...scopedMissing.map(({ path, unavailablePath }) => ({
|
|
149
|
+
path: `/${scope}${path}`, unavailablePath: `/${scope}${unavailablePath}`,
|
|
150
|
+
})));
|
|
151
|
+
let materialized = { ...emptyState(), ...patched };
|
|
144
152
|
// Authored operations first, then the same-scope intent ownership cascade.
|
|
145
153
|
const cascade = cascades[scope] = computeIntentCascade(scope, currentStates[scope], materialized);
|
|
146
154
|
if (cascade.length > 0)
|
|
147
155
|
materialized = applyPatch(materialized, cascadeDeletionPatch(cascade));
|
|
148
156
|
compileReadArtifacts(materialized, { artifacts: authored.artifacts ?? {} }, artifactReads.filter((read) => (read.scope ?? "global") === scope), provenanceUpdates[scope]);
|
|
149
157
|
compileReadSkills(scope, materialized, { artifacts: authored.artifacts ?? {} }, skillReads.filter((read) => read.scope === scope), provenanceUpdates[scope]);
|
|
150
|
-
validateMaterializedTransition(materialized, scope);
|
|
151
158
|
const nextState = materialized;
|
|
152
159
|
for (const key of Object.keys(emptyState())) {
|
|
153
|
-
if (!Object.hasOwn(
|
|
160
|
+
if (!Object.hasOwn(patched, key)
|
|
154
161
|
&& !(key === "artifacts" && Object.keys(provenanceUpdates[scope]).length > 0))
|
|
155
162
|
delete nextState[key];
|
|
156
163
|
}
|
|
157
|
-
|
|
164
|
+
const normalized = normalize ? pruneEmptyObjects(nextState) : nextState;
|
|
165
|
+
validateMaterializedTransition({ ...emptyState(), ...normalized }, scope);
|
|
166
|
+
nextStates[scope] = normalized;
|
|
158
167
|
}
|
|
159
168
|
return {
|
|
160
169
|
nextStates,
|
|
170
|
+
missingDeletions,
|
|
161
171
|
cascades,
|
|
162
172
|
provenanceUpdates,
|
|
163
173
|
stateHashes: {
|
|
@@ -56,7 +56,7 @@ Call `patch_state` alone per assistant response; await acceptance before depende
|
|
|
56
56
|
|
|
57
57
|
The runtime waits cancelably for publication ownership, then applies authored Global/CWD operations to current canonical values. Untouched fields survive; overlapping targets follow successful acceptance order. Correct repeats succeed as `State already current.` without another semantic revision. Do not repeat external actions during a memory wait, or rebuild an entire scope from an older snapshot. Session ownership/history fences remain private, not a universal merge.
|
|
58
58
|
|
|
59
|
-
When present, semantic planes `intents`, `contract`, `working`, `artifacts`, and `lazy` are objects; nested lazy values may contain ordinary JSON without stored nulls. Stored checkpoints and patches may omit any documented plane. Current and historical views assemble only known fields present in the selected scopes. Absent fields and empty responses are omitted from views. Checkpoint/tail readers ignore unknown top-level fields, and writers emit only known fields. Nested data within known planes remains intact. Explicit value reads of an absent documented top-level field return `null`. Authored `patch_state` keeps its documented field grammar. Objects merge, arrays/scalars replace, omitted fields persist. Nested `null` removes an owned object key; inherited content may reappear. Canonical `"[N]"` keys patch array elements; indexed deletion
|
|
59
|
+
When present, semantic planes `intents`, `contract`, `working`, `artifacts`, and `lazy` are objects; nested lazy values may contain ordinary JSON without stored nulls. Stored checkpoints and patches may omit any documented plane. Current and historical views assemble only known fields present in the selected scopes. Absent fields and empty responses are omitted from views. Checkpoint/tail readers ignore unknown top-level fields, and writers emit only known fields. Nested data within known planes remains intact. Explicit value reads of an absent documented top-level field return `null`. Authored `patch_state` keeps its documented field grammar. Objects merge, arrays/scalars replace, and omitted nonempty fields persist. Accepted supplied scopes recursively omit empty object fields and planes, including explicit `{}`; the scope root remains `{}`. This also cleans legacy empty branches in that scope, which can advance its revision once. Array slots and empty arrays remain; empty object fields inside elements disappear without shifting indices. Model projection hides legacy empty branches, but exact current/historical reads and untouched scopes are not normalized. Nested `null` removes an owned object key; empty ancestors disappear and inherited content may reappear. Canonical `"[N]"` keys patch existing array elements; indexed deletion and out-of-bounds indices are forbidden. Replace the whole array to add/remove elements. Missing authored object-key deletions succeed with a nonfatal hint showing owner-scoped JSON Pointers for the target and first unavailable component. Hints are not state, do not advance revisions and do not prove a typo; check the address only when deletion was expected to change something. Never guess or automatically correct the target.
|
|
60
60
|
|
|
61
61
|
Work from intents. A structured `{"$ref"}` anywhere inside an intent owns ("delete with me") an existing object key under `working` or `lazy` in the same scope; a textual `$path` mention only uses it. Opening a task:
|
|
62
62
|
|
|
@@ -66,7 +66,7 @@ Later scopes win. A scope-local `null` deletion removes only that scope's key an
|
|
|
66
66
|
- `readTemporalState` and the embedding callback keep default-bearing compatibility views for internal registry consumers. Those defaults never become stored overrides or model context.
|
|
67
67
|
- Passive reads and Start do not fill checkpoint fields or create revisions for normalization; missing shared pairs initialize as empty objects.
|
|
68
68
|
|
|
69
|
-
**Staging and replay.** Authored staging uses raw scope presence and derives runtime-normalized replay from the writes actually accepted, including complete artifact replacement; replay must reproduce accepted state exactly.
|
|
69
|
+
**Staging and replay.** Authored staging uses raw scope presence and derives runtime-normalized replay from the writes actually accepted, including complete artifact replacement; replay must reproduce accepted state exactly. Each supplied scope (and Session on response reconciliation) recursively omits empty object fields, including explicit `{}` and empty planes, before intent ownership and after cascades/compilation. The scope root remains `{}`; array slots and empty arrays remain, while object fields inside array elements are pruned. Untouched scopes are not normalized. Existing empty branches in a supplied scope become explicit accepted deletions and may advance its revision once; clean repeats create none. Model projection hides legacy empty object fields without changing stored state, exact current/historical reads or replay.
|
|
70
70
|
|
|
71
71
|
**Validation.** Present documented fields keep usable object/string types, valid artifact entries and the no-stored-null rule; ignored fields may contain arbitrary JSON. Malformed JSON and invalid storage/history authority remain errors.
|
|
72
72
|
|
|
@@ -701,7 +701,9 @@ The package exposes read-only host contracts that:
|
|
|
701
701
|
**`patch_state` grammar.** It accepts one or more fixed `global`, `cwd` and `session` semantic patches.
|
|
702
702
|
|
|
703
703
|
- Supplied scopes contain object-valued `artifacts`, `contract`, `working` and `intents`, plus object-valued `lazy` whose nested values are ordinary JSON.
|
|
704
|
-
-
|
|
704
|
+
- Recursive object merge updates fields, arrays/primitives replace, and nested object-key `null` deletes, including inside newly created objects. Deletion-only patches through absent ancestors create no empty parents. Accepted supplied scopes recursively omit empty object fields and empty planes: deleting the last child removes its empty ancestors, and explicit `{}` is not a retained value. Nonempty omitted fields persist; an object patch that turns a scalar/array into an empty object removes that field. Cleanup is scope-local and can reveal inherited values; it stops at the scope root and never removes array slots.
|
|
705
|
+
- Against an existing array, a nonempty object containing only canonical `"[N]"` keys recursively patches those existing elements (zero-based). Out-of-bounds indices and indexed `null` deletions are rejected atomically; adding/removing elements requires replacing the whole array. Against objects, those keys remain literal object keys.
|
|
706
|
+
- Missing authored object-key deletions succeed with nonfatal hints in the acknowledgement, for both unchanged and mixed patches. Each hint gives the owner-scoped target and first unavailable component as escaped JSON Pointers; a non-object ancestor is also unavailable. These hints are presentation-only: no state fields, revisions, history records, automatic path correction or cascade warnings.
|
|
705
707
|
- Rejected: materialized null within documented semantic planes, empty supplied scopes, unknown top-level fields, model-authored `response`, and finalization or `{scope, patch}` / `unchanged` grammars. The semantic null rule does not apply to runtime envelopes such as an origin's null parent.
|
|
706
708
|
- Correct repeated results succeed as `State already current.` without another semantic revision or history record; avoid gratuitous acknowledgment patches.
|
|
707
709
|
- Global/CWD overlap follows successful acceptance order, and unmentioned current fields survive. Session ownership is not a shared-memory merge.
|
|
@@ -224,6 +224,7 @@ When `pi-telegram` is available, its main-menu section shows `State Flow: active
|
|
|
224
224
|
**Memory inspection:**
|
|
225
225
|
|
|
226
226
|
- Owner-scope Rich snapshots show `#revision`; Effective shows the vector.
|
|
227
|
+
- 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.
|
|
227
228
|
- During a failed-Passive write fence, memory-enabled inspection uses the accepted cache without a potentially conflicting refresh.
|
|
228
229
|
- Otherwise, inspection waits cancelably to load or refresh one coherent shared view, even when passive model tools are disabled. It does not initialize, publish or advance storage.
|
|
229
230
|
- Data and displayed revisions come from the same observation; absent or invalid memory stays unavailable.
|
|
@@ -44,9 +44,13 @@ function lazyValueKind(value: JsonValue): LazyValueKind {
|
|
|
44
44
|
return typeof value as Exclude<LazyValueKind, "array" | "object">;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function hasLazyContent(value: JsonValue): boolean {
|
|
48
|
+
return !isObject(value) || Object.values(value).some(hasLazyContent);
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
/** Fixed-budget navigation only: never place lazy bodies or partial key catalogs in baseline context. */
|
|
48
52
|
export function lazyNavigationHint(state: SemanticState): { available: boolean; path: string; keys?: Record<string, LazyValueKind> } {
|
|
49
|
-
const entries = isObject(state.lazy) ? Object.entries(state.lazy) : [];
|
|
53
|
+
const entries = isObject(state.lazy) ? Object.entries(state.lazy).filter(([, value]) => hasLazyContent(value)) : [];
|
|
50
54
|
const base = { available: entries.length > 0, path: LAZY_HINT_PATH };
|
|
51
55
|
if (!base.available) return base;
|
|
52
56
|
if (entries.length > LAZY_HINT_MAX_KEYS) return base;
|
|
@@ -184,6 +188,7 @@ export class ContextProjection {
|
|
|
184
188
|
const prefix = (a: readonly (string | number)[], b: readonly (string | number)[]) =>
|
|
185
189
|
a.length <= b.length && a.every((part, index) => part === b[index]);
|
|
186
190
|
updates.effective = updates.effective.filter((entry) => {
|
|
191
|
+
if ("deleted" in entry && this.view && known(entry.path) === undefined) return false;
|
|
187
192
|
const matches = ({ path }: typeof leaves[number]) => path.length === entry.path.length && prefix(path, entry.path);
|
|
188
193
|
const authored = leaves.findLast(matches) ?? objects.findLast(matches);
|
|
189
194
|
if (!authored) return true;
|
|
@@ -815,9 +815,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
815
815
|
? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
|
|
816
816
|
: "\nState already current.";
|
|
817
817
|
return { content: [
|
|
818
|
-
{ type: "text" as const, text: acknowledgement }
|
|
818
|
+
{ type: "text" as const, text: acknowledgement + stage.missingDeletions.map(({ path, unavailablePath }) =>
|
|
819
|
+
`\n\nHint: deletion skipped at ${path}; target absent. First unavailable component: ${unavailablePath}. Check the path if you expected an existing value.`).join("") },
|
|
819
820
|
...(updates ? [{ type: "text" as const, text: `\n${presentationJson({ state_updates: updates })}` }] : []),
|
|
820
|
-
], details: { scopes, step: snapshot.meta.step, changed } };
|
|
821
|
+
], details: { scopes, step: snapshot.meta.step, changed, missingDeletions: stage.missingDeletions } };
|
|
821
822
|
}, signal);
|
|
822
823
|
} catch (error) {
|
|
823
824
|
let attempted: unknown;
|
|
@@ -3,6 +3,12 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
|
|
4
4
|
export interface JsonObject { [key: string]: JsonValue }
|
|
5
5
|
|
|
6
|
+
export interface MissingDeletion {
|
|
7
|
+
/** JSON Pointer paths relative to the patched object. */
|
|
8
|
+
path: string;
|
|
9
|
+
unavailablePath: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
const ARRAY_INDEX_SELECTOR = /^\[(0|[1-9]\d*)\]$/;
|
|
7
13
|
|
|
8
14
|
function isIndexedArrayPatch(value: JsonObject): boolean {
|
|
@@ -10,17 +16,22 @@ function isIndexedArrayPatch(value: JsonObject): boolean {
|
|
|
10
16
|
return keys.length > 0 && keys.every((key) => ARRAY_INDEX_SELECTOR.test(key));
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
function applyOwnedValue(
|
|
19
|
+
function applyOwnedValue(
|
|
20
|
+
current: JsonValue | undefined, value: JsonValue, owned: boolean,
|
|
21
|
+
missingDeletions: MissingDeletion[] | undefined, path: string, unavailablePath?: string,
|
|
22
|
+
): JsonValue {
|
|
14
23
|
// Preserve inherited-object merge semantics without borrowing prototype objects.
|
|
15
24
|
if (!owned && current !== null && typeof current === "object") current = structuredClone(current);
|
|
16
25
|
return Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
|
|
17
|
-
? applyOwnedArrayPatch(current, value)
|
|
18
|
-
: isObject(
|
|
19
|
-
? applyOwnedPatch(current, value)
|
|
26
|
+
? applyOwnedArrayPatch(current, value, missingDeletions, path, unavailablePath)
|
|
27
|
+
: isObject(value)
|
|
28
|
+
? applyOwnedPatch(isObject(current) ? current : {}, value, missingDeletions, path, unavailablePath ?? (!isObject(current) ? path : undefined))
|
|
20
29
|
: structuredClone(value);
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
function applyOwnedArrayPatch(
|
|
32
|
+
function applyOwnedArrayPatch(
|
|
33
|
+
state: JsonValue[], patch: JsonObject, missingDeletions: MissingDeletion[] | undefined, path: string, unavailablePath?: string,
|
|
34
|
+
): JsonValue[] {
|
|
24
35
|
let next = state;
|
|
25
36
|
for (const [selector, value] of Object.entries(patch)) {
|
|
26
37
|
const index = Number(ARRAY_INDEX_SELECTOR.exec(selector)![1]);
|
|
@@ -30,7 +41,7 @@ function applyOwnedArrayPatch(state: JsonValue[], patch: JsonObject): JsonValue[
|
|
|
30
41
|
if (value === null) throw new Error(`State patch array index ${selector} cannot be deleted; replace the whole array instead`);
|
|
31
42
|
const owns = Object.hasOwn(next, index);
|
|
32
43
|
const current = next[index];
|
|
33
|
-
const materialized = applyOwnedValue(current, value, owns);
|
|
44
|
+
const materialized = applyOwnedValue(current, value, owns, missingDeletions, `${path}/${index}`, unavailablePath);
|
|
34
45
|
if (owns && Object.is(current, materialized)) continue;
|
|
35
46
|
if (next === state) next = state.slice();
|
|
36
47
|
next[index] = materialized;
|
|
@@ -38,18 +49,27 @@ function applyOwnedArrayPatch(state: JsonValue[], patch: JsonObject): JsonValue[
|
|
|
38
49
|
return next;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
function applyOwnedPatch(
|
|
52
|
+
function applyOwnedPatch(
|
|
53
|
+
state: JsonObject, patch: JsonObject, missingDeletions?: MissingDeletion[], path = "", unavailablePath?: string,
|
|
54
|
+
): JsonObject {
|
|
42
55
|
let next = state;
|
|
43
56
|
for (const [key, value] of Object.entries(patch)) {
|
|
44
57
|
const owns = Object.hasOwn(next, key);
|
|
58
|
+
const target = missingDeletions ? `${path}/${key.replace(/~/g, "~0").replace(/\//g, "~1")}` : "";
|
|
45
59
|
if (value === null) {
|
|
46
|
-
if (!owns)
|
|
60
|
+
if (!owns) {
|
|
61
|
+
missingDeletions?.push({ path: target, unavailablePath: unavailablePath ?? target });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
47
64
|
if (next === state) next = { ...state };
|
|
48
65
|
delete next[key];
|
|
49
66
|
continue;
|
|
50
67
|
}
|
|
51
68
|
const current = next[key];
|
|
52
|
-
const materialized = applyOwnedValue(current, value, owns);
|
|
69
|
+
const materialized = applyOwnedValue(current, value, owns, missingDeletions, target, unavailablePath ?? (!owns ? target : undefined));
|
|
70
|
+
// Deletion-only patches must not fabricate absent ancestor objects; explicit {} still writes.
|
|
71
|
+
if (!owns && current === undefined && isObject(value) && Object.keys(value).length > 0
|
|
72
|
+
&& isObject(materialized) && Object.keys(materialized).length === 0) continue;
|
|
53
73
|
if (owns && Object.is(current, materialized)) continue;
|
|
54
74
|
if (next === state) next = { ...state };
|
|
55
75
|
Object.defineProperty(next, key, { value: materialized, enumerable: true, configurable: true, writable: true });
|
|
@@ -57,9 +77,39 @@ function applyOwnedPatch(state: JsonObject, patch: JsonObject): JsonObject {
|
|
|
57
77
|
return next;
|
|
58
78
|
}
|
|
59
79
|
|
|
80
|
+
/** Remove empty object fields without removing array slots or mutating the input. */
|
|
81
|
+
export function pruneEmptyObjects(value: JsonObject): JsonObject;
|
|
82
|
+
export function pruneEmptyObjects(value: JsonValue): JsonValue;
|
|
83
|
+
export function pruneEmptyObjects(value: JsonValue): JsonValue {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
let next = value;
|
|
86
|
+
for (let index = 0; index < value.length; index++) {
|
|
87
|
+
const pruned = pruneEmptyObjects(value[index]!);
|
|
88
|
+
if (pruned === value[index]) continue;
|
|
89
|
+
if (next === value) next = value.slice();
|
|
90
|
+
next[index] = pruned;
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
if (!isObject(value)) return value;
|
|
95
|
+
let next = value as JsonObject;
|
|
96
|
+
for (const [key, child] of Object.entries(value)) {
|
|
97
|
+
const pruned = pruneEmptyObjects(child);
|
|
98
|
+
const empty = isObject(pruned) && Object.keys(pruned).length === 0;
|
|
99
|
+
if (!empty && pruned === child) continue;
|
|
100
|
+
if (next === value) next = { ...value };
|
|
101
|
+
if (empty) delete next[key];
|
|
102
|
+
else Object.defineProperty(next, key, { value: pruned, enumerable: true, configurable: true, writable: true });
|
|
103
|
+
}
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
|
|
60
107
|
/** Detach at the mutable public boundary; share untouched paths only inside the owned draft. */
|
|
61
|
-
export function applyPatch(state: JsonObject, patch: JsonObject): JsonObject {
|
|
62
|
-
|
|
108
|
+
export function applyPatch(state: JsonObject, patch: JsonObject, missingDeletions?: MissingDeletion[]): JsonObject {
|
|
109
|
+
const collected = missingDeletions ? [] as MissingDeletion[] : undefined;
|
|
110
|
+
const next = applyOwnedPatch(structuredClone(state), patch, collected);
|
|
111
|
+
if (collected) missingDeletions!.push(...collected);
|
|
112
|
+
return next;
|
|
63
113
|
}
|
|
64
114
|
|
|
65
115
|
export function isObject(value: JsonValue | unknown): value is JsonObject {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, type RecentScopePatch } from "./history.ts";
|
|
2
2
|
import { isObject, sameJson, type JsonObject, type JsonValue } from "./json.ts";
|
|
3
|
-
import { emptyState, projectSemanticPatch,
|
|
3
|
+
import { emptyState, projectSemanticPatch, projectStateForRead, type SemanticState, type StateScope } from "./state.ts";
|
|
4
4
|
import { readTemporalView, type TemporalState, type TransitionBoundary } from "./temporal.ts";
|
|
5
5
|
|
|
6
6
|
export type StateReadQuery =
|
|
@@ -61,7 +61,7 @@ export function readStatePath(view: TemporalState, path: string, historyLimit =
|
|
|
61
61
|
if (query.kind === "state") {
|
|
62
62
|
const boundary = view.lineage[view.lineage.length - 1 - query.offset];
|
|
63
63
|
if (!boundary) throw new Error("Requested history predates the proven temporal origin");
|
|
64
|
-
return { path, boundary: structuredClone(boundary), state:
|
|
64
|
+
return { path, boundary: structuredClone(boundary), state: projectStateForRead(readTemporalView(view, query.offset, query.scope, historyLimit)) };
|
|
65
65
|
}
|
|
66
66
|
const record = view.scopes[query.scope].patches.at(-1 - query.offset);
|
|
67
67
|
if (!record) throw new Error(`Requested ${query.scope} patch predates retained hot history`);
|
|
@@ -275,7 +275,7 @@ export function readProjectedState(view: TemporalState, paths: readonly string[]
|
|
|
275
275
|
if (query.kind !== "state") throw new Error("Value and keys projections require a state path");
|
|
276
276
|
const readsLazy = selectors[0]?.kind === "key" && selectors[0].key === "lazy";
|
|
277
277
|
const raw = readTemporalView(view, query.offset, query.scope, historyLimit);
|
|
278
|
-
const state = readsLazy ? raw :
|
|
278
|
+
const state = readsLazy ? raw : projectStateForRead(raw);
|
|
279
279
|
const field = selectors.length === 1 && selectors[0]?.kind === "key" ? selectors[0].key : undefined;
|
|
280
280
|
if (projection === "value" && field !== undefined && Object.hasOwn(emptyState(), field) && !Object.hasOwn(state, field)) return { value: null };
|
|
281
281
|
return projectValue(selectValue(state, selectors, path), projection);
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ArtifactModelHints,
|
|
7
7
|
type ArtifactRegistry,
|
|
8
8
|
} from "./artifact.ts";
|
|
9
|
-
import { applyPatch, isObject, type JsonObject } from "./json.ts";
|
|
9
|
+
import { applyPatch, isObject, pruneEmptyObjects, type JsonObject } from "./json.ts";
|
|
10
10
|
|
|
11
11
|
/** Runtime defaults for documented semantic planes; stored objects may omit them or retain other fields. */
|
|
12
12
|
export type MaterializedState = JsonObject & {
|
|
@@ -148,10 +148,15 @@ export function projectSemanticPatch(patch: JsonObject): JsonObject {
|
|
|
148
148
|
return projected;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
/**
|
|
152
|
-
export function
|
|
151
|
+
/** Explicit hot reads filter hidden bodies/bookkeeping without normalizing legacy semantic data. */
|
|
152
|
+
export function projectStateForRead(state: SemanticState, artifactHints: ArtifactModelHints = {}): SemanticState {
|
|
153
153
|
const { lazy: _hidden, ...visible } = state;
|
|
154
154
|
const projected = projectSemanticState(visible);
|
|
155
155
|
if (projected.artifacts) projected.artifacts = projectArtifactsForModel(projected.artifacts, artifactHints);
|
|
156
156
|
return projected;
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
/** Ordinary model context omits empty object fields; exact reads and replay remain observational. */
|
|
160
|
+
export function projectStateForModel(state: SemanticState, artifactHints: ArtifactModelHints = {}): SemanticState {
|
|
161
|
+
return pruneEmptyObjects(projectStateForRead(state, artifactHints));
|
|
162
|
+
}
|
|
@@ -191,34 +191,31 @@ const STATE_FLOW_SCOPE_LABELS: Record<StateFlowTelegramScope, string> = {
|
|
|
191
191
|
effective: "🧬 Effective",
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
194
|
+
// Budget the transport-serialized text, leaving room for six fields and notices
|
|
195
|
+
// below Telegram's 32,768-character Rich message ceiling.
|
|
196
196
|
const STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS = 3_000;
|
|
197
197
|
|
|
198
|
-
function renderStateFlowTelegramField(value: unknown):
|
|
198
|
+
function renderStateFlowTelegramField(value: unknown): StateFlowTelegramRichBlock[] {
|
|
199
199
|
const json = JSON.stringify(value, null, 2);
|
|
200
|
-
if (json.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS)
|
|
200
|
+
if (JSON.stringify(json).length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
201
|
+
return [{ type: "pre", language: "json", text: json }];
|
|
202
|
+
}
|
|
201
203
|
let low = 0;
|
|
202
|
-
let high = json.length;
|
|
203
|
-
let rendered = "";
|
|
204
|
+
let high = Math.min(json.length, STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS);
|
|
204
205
|
while (low <= high) {
|
|
205
206
|
const length = Math.floor((low + high) / 2);
|
|
206
|
-
|
|
207
|
-
truncated: true,
|
|
208
|
-
...(value !== null && typeof value === "object" && !Array.isArray(value)
|
|
209
|
-
? { keys: Object.keys(value) }
|
|
210
|
-
: {}),
|
|
211
|
-
preview: json.slice(0, length),
|
|
212
|
-
omittedChars: json.length - length,
|
|
213
|
-
}, null, 2);
|
|
214
|
-
if (candidate.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
215
|
-
rendered = candidate;
|
|
207
|
+
if (JSON.stringify(json.slice(0, length)).length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
216
208
|
low = length + 1;
|
|
217
209
|
} else {
|
|
218
210
|
high = length - 1;
|
|
219
211
|
}
|
|
220
212
|
}
|
|
221
|
-
|
|
213
|
+
// Do not split a Unicode surrogate pair at the preview boundary.
|
|
214
|
+
if (/[\uD800-\uDBFF]/.test(json.charAt(high - 1))) high -= 1;
|
|
215
|
+
return [
|
|
216
|
+
{ type: "pre", language: "json", text: json.slice(0, high) },
|
|
217
|
+
{ type: "pre", text: `Truncated preview — ${json.length - high} characters omitted.` },
|
|
218
|
+
];
|
|
222
219
|
}
|
|
223
220
|
|
|
224
221
|
export function renderStateFlowRichState(scope: StateFlowTelegramScope, revisions: ScopeRevisions, state: StateFlowTelegramState): StateFlowTelegramRichMessage {
|
|
@@ -238,7 +235,7 @@ export function renderStateFlowRichState(scope: StateFlowTelegramScope, revision
|
|
|
238
235
|
...fields.filter((field) => state[field] !== undefined && state[field] !== "").map((field) => ({
|
|
239
236
|
type: "details" as const,
|
|
240
237
|
summary: { type: "code" as const, text: field },
|
|
241
|
-
blocks:
|
|
238
|
+
blocks: renderStateFlowTelegramField(state[field]),
|
|
242
239
|
})),
|
|
243
240
|
],
|
|
244
241
|
skip_entity_detection: true,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./artifact.ts";
|
|
10
10
|
import type { SuccessfulArtifactRead } from "./acquisition.ts";
|
|
11
11
|
import { createAcceptedTransition, type AcceptedTransition } from "./history.ts";
|
|
12
|
-
import { applyPatch, containsNull, hashJson, isObject, validatePatch, type JsonObject } from "./json.ts";
|
|
12
|
+
import { applyPatch, containsNull, hashJson, isObject, pruneEmptyObjects, validatePatch, type JsonObject, type MissingDeletion } from "./json.ts";
|
|
13
13
|
import { cascadeDeletionPatch, computeIntentCascade, type OwnedPath } from "./ownership.ts";
|
|
14
14
|
import { hasCompiledSkillArtifact, SKILL_ARTIFACT_COMPILER, type SuccessfulSkillRead } from "./skills.ts";
|
|
15
15
|
import type { Snapshot } from "./snapshot.ts";
|
|
@@ -29,6 +29,8 @@ import type {
|
|
|
29
29
|
|
|
30
30
|
export interface StagedScopedTransition {
|
|
31
31
|
nextStates: ScopedSemanticStates;
|
|
32
|
+
/** Authored no-op deletions; presentation evidence only, never replay input. */
|
|
33
|
+
missingDeletions: MissingDeletion[];
|
|
32
34
|
/** Scope-local targets removed by the staged intent cascade, not replay input. */
|
|
33
35
|
cascades: Record<StateScope, OwnedPath[]>;
|
|
34
36
|
stateHashes: Record<StateScope, string>;
|
|
@@ -189,10 +191,18 @@ function stageScopedSemanticTransition(
|
|
|
189
191
|
const nextStates = { ...currentStates };
|
|
190
192
|
const provenanceUpdates: Record<StateScope, Record<string, ArtifactProvenance>> = { global: {}, cwd: {}, session: {} };
|
|
191
193
|
const cascades: Record<StateScope, OwnedPath[]> = { global: [], cwd: [], session: [] };
|
|
194
|
+
const missingDeletions: MissingDeletion[] = [];
|
|
192
195
|
for (const scope of SCOPES) {
|
|
193
196
|
const authored = patches.get(scope) ?? {};
|
|
194
197
|
const patch = { ...authored, ...(scope === "session" && acceptedResponse !== undefined ? { response: acceptedResponse } : {}) };
|
|
195
|
-
|
|
198
|
+
const scopedMissing: MissingDeletion[] = [];
|
|
199
|
+
const normalize = patches.has(scope) || scope === "session" && acceptedResponse !== undefined;
|
|
200
|
+
const applied = applyPatch(currentStates[scope], patch, scopedMissing);
|
|
201
|
+
const patched = normalize ? pruneEmptyObjects(applied) : applied;
|
|
202
|
+
missingDeletions.push(...scopedMissing.map(({ path, unavailablePath }) => ({
|
|
203
|
+
path: `/${scope}${path}`, unavailablePath: `/${scope}${unavailablePath}`,
|
|
204
|
+
})));
|
|
205
|
+
let materialized = { ...emptyState(), ...patched } as StateDocument;
|
|
196
206
|
// Authored operations first, then the same-scope intent ownership cascade.
|
|
197
207
|
const cascade = cascades[scope] = computeIntentCascade(scope, currentStates[scope], materialized);
|
|
198
208
|
if (cascade.length > 0) materialized = applyPatch(materialized, cascadeDeletionPatch(cascade)) as StateDocument;
|
|
@@ -203,16 +213,18 @@ function stageScopedSemanticTransition(
|
|
|
203
213
|
provenanceUpdates[scope],
|
|
204
214
|
);
|
|
205
215
|
compileReadSkills(scope, materialized, { artifacts: authored.artifacts ?? {} }, skillReads.filter((read) => read.scope === scope), provenanceUpdates[scope]);
|
|
206
|
-
validateMaterializedTransition(materialized, scope);
|
|
207
216
|
const nextState: SemanticState = materialized;
|
|
208
217
|
for (const key of Object.keys(emptyState())) {
|
|
209
|
-
if (!Object.hasOwn(
|
|
218
|
+
if (!Object.hasOwn(patched, key)
|
|
210
219
|
&& !(key === "artifacts" && Object.keys(provenanceUpdates[scope]).length > 0)) delete nextState[key];
|
|
211
220
|
}
|
|
212
|
-
|
|
221
|
+
const normalized = normalize ? pruneEmptyObjects(nextState) : nextState;
|
|
222
|
+
validateMaterializedTransition({ ...emptyState(), ...normalized }, scope);
|
|
223
|
+
nextStates[scope] = normalized;
|
|
213
224
|
}
|
|
214
225
|
return {
|
|
215
226
|
nextStates,
|
|
227
|
+
missingDeletions,
|
|
216
228
|
cascades,
|
|
217
229
|
provenanceUpdates,
|
|
218
230
|
stateHashes: {
|
|
@@ -56,7 +56,7 @@ Call `patch_state` alone per assistant response; await acceptance before depende
|
|
|
56
56
|
|
|
57
57
|
The runtime waits cancelably for publication ownership, then applies authored Global/CWD operations to current canonical values. Untouched fields survive; overlapping targets follow successful acceptance order. Correct repeats succeed as `State already current.` without another semantic revision. Do not repeat external actions during a memory wait, or rebuild an entire scope from an older snapshot. Session ownership/history fences remain private, not a universal merge.
|
|
58
58
|
|
|
59
|
-
When present, semantic planes `intents`, `contract`, `working`, `artifacts`, and `lazy` are objects; nested lazy values may contain ordinary JSON without stored nulls. Stored checkpoints and patches may omit any documented plane. Current and historical views assemble only known fields present in the selected scopes. Absent fields and empty responses are omitted from views. Checkpoint/tail readers ignore unknown top-level fields, and writers emit only known fields. Nested data within known planes remains intact. Explicit value reads of an absent documented top-level field return `null`. Authored `patch_state` keeps its documented field grammar. Objects merge, arrays/scalars replace, omitted fields persist. Nested `null` removes an owned object key; inherited content may reappear. Canonical `"[N]"` keys patch array elements; indexed deletion
|
|
59
|
+
When present, semantic planes `intents`, `contract`, `working`, `artifacts`, and `lazy` are objects; nested lazy values may contain ordinary JSON without stored nulls. Stored checkpoints and patches may omit any documented plane. Current and historical views assemble only known fields present in the selected scopes. Absent fields and empty responses are omitted from views. Checkpoint/tail readers ignore unknown top-level fields, and writers emit only known fields. Nested data within known planes remains intact. Explicit value reads of an absent documented top-level field return `null`. Authored `patch_state` keeps its documented field grammar. Objects merge, arrays/scalars replace, and omitted nonempty fields persist. Accepted supplied scopes recursively omit empty object fields and planes, including explicit `{}`; the scope root remains `{}`. This also cleans legacy empty branches in that scope, which can advance its revision once. Array slots and empty arrays remain; empty object fields inside elements disappear without shifting indices. Model projection hides legacy empty branches, but exact current/historical reads and untouched scopes are not normalized. Nested `null` removes an owned object key; empty ancestors disappear and inherited content may reappear. Canonical `"[N]"` keys patch existing array elements; indexed deletion and out-of-bounds indices are forbidden. Replace the whole array to add/remove elements. Missing authored object-key deletions succeed with a nonfatal hint showing owner-scoped JSON Pointers for the target and first unavailable component. Hints are not state, do not advance revisions and do not prove a typo; check the address only when deletion was expected to change something. Never guess or automatically correct the target.
|
|
60
60
|
|
|
61
61
|
Work from intents. A structured `{"$ref"}` anywhere inside an intent owns ("delete with me") an existing object key under `working` or `lazy` in the same scope; a textual `$path` mention only uses it. Opening a task:
|
|
62
62
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.5",
|
|
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.
|
|
49
|
+
"@llblab/pi-state-flow": "0.25.4",
|
|
50
50
|
"@llblab/pi-telegram": "0.51.6",
|
|
51
51
|
"@llblab/skills": "1.15.0"
|
|
52
52
|
},
|