Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions loopx/control_plane/quota/context_delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,12 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r
return publish(request, encodeContextDelivery(request, legacyQuotaFields, values), values);
}

/** Progressive file deltas expose current discovery even without a retained model baseline. */
export function fileNavigationPaths(context: JsonObject): Path[] {
const details = jsonObject(context.context_details);
return details && Object.hasOwn(details, "unread_context_index") ? [["context_details", "unread_context_index"]] : [];
}

/** Shared encoding within one presentation transaction; callers publish once
* against their original source so numeric facts never round-trip through JS. */
export function encodeContextDelivery(request: JsonObject, legacyQuotaFields: readonly string[],
Expand Down Expand Up @@ -675,13 +681,21 @@ export function restoreContextDelivery(request: JsonObject, legacyQuotaFields: r
|| delivery.ancestry !== Number(baseline.ancestry) + 1 || Number(delivery.ancestry) > MAX_ANCESTRY) return fallback();
const before = freshPaths(context, surface, legacyQuotaFields, base), after = freshPaths(currentContext, surface, legacyQuotaFields, target);
if (!fileBacked && (!before.valid || !after.valid)) return fallback();
const expected = changeTree(context, currentContext, fileBacked ? [] : after.paths, fileBacked,
const currentChanges = (paths: Path[]) => changeTree(context, currentContext, paths, fileBacked,
{identity, before: ["baseline", "context"], after: ["current", "context"],
...(request.source_edits === true ? {values} : {})});
// Also rejects omitted unchanged controls, redundant/conflicting edits,
// malformed array indices/lengths and unsupported operations before applying.
if (contextProjectionIdentity(request, {changes: delivery.changes}, request.source_number_tokens, values)
!== contextProjectionIdentity(request, {changes: expected}, request.source_number_tokens, values)) return fallback();
const wireIdentity = contextProjectionIdentity(request, {changes: delivery.changes}, request.source_number_tokens, values);
const matches = (changes: ReturnType<typeof changeTree>) =>
contextProjectionIdentity(request, {changes}, request.source_number_tokens, values) === wireIdentity;
const navigation = fileBacked ? fileNavigationPaths(currentContext) : [];
let expected = currentChanges(fileBacked ? navigation : after.paths);
if (!matches(expected)) {
// The ordinary codec may encode the same file without progressive
// navigation. Verify that exact encoding too; never apply caller edits.
if (!navigation.length) return fallback();
expected = currentChanges([]);
if (!matches(expected)) return fallback();
}
const result = applyTree(context, expected);
if (!equal(result, currentContext)) return fallback();
return publish(request, {...copy(delivery), context: copy(currentContext)}, values);
Expand Down
7 changes: 2 additions & 5 deletions loopx/control_plane/quota/progressive_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {GOAL_OUTCOMES_SCHEMA, OUTCOME_CONTEXT_NAVIGATION_SCHEMA} from "../goals/
import {FULL_OUTCOME_READ_GUIDANCE, PROGRESSIVE_OUTCOME_READ_GUIDANCE} from "../work_items/interaction_contract.ts";
import {quotaSummaryReferences, contextSourceIdentity, contextProjectionChanges,
type ProjectionSourceValues} from "./diagnostic_projection.ts";
import {encodeContextDelivery} from "./context_delivery.ts";
import {encodeContextDelivery, fileNavigationPaths} from "./context_delivery.ts";
import {createHash} from "node:crypto";

type Path = (string | number)[];
Expand Down Expand Up @@ -244,10 +244,7 @@ export function prepareProgressiveContextDelivery(request: JsonObject, legacyFie
const required = requiredNativeContext(request, sourceValues);
const codec = nativeContextContinuity({...request, context: required.context,
...(request.source_number_tokens === undefined ? {} : {source_number_tokens: required.tokens})});
const details = jsonObject(required.context.context_details);
const navigation: Path[] = details && Object.hasOwn(details, "unread_context_index")
? [["context_details", "unread_context_index"]] : [];
const delivery = encodeContextDelivery(codec, legacyFields, sourceValues, navigation);
const delivery = encodeContextDelivery(codec, legacyFields, sourceValues, fileNavigationPaths(required.context));
const {operation: _operation, source_edits: _edits, source_number_tokens: _numbers, ...source} = request;
return contextProjectionChanges(source, {context: required.context, delivery, projected: required.projected,
read_sections: inlineReadSections(request, required.context)}, sourceValues);
Expand Down
10 changes: 10 additions & 0 deletions tests/control_plane/test_context_unread_reachability.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ def read_detail(source, pointer):
_, warm = project(tmp_path, "next-turn", payload=current)
assert warm["mode"] == "delta"
required = json.loads(Path(warm["recovery"]["path"]).read_bytes())
from loopx.control_plane.quota.turn_envelope import restore_context_delivery

current_capture = {"schema_version": warm["schema_version"], "mode": "full",
"target": warm["target"], "context": required}
restored = restore_context_delivery(warm, baseline=first, current=current_capture)
assert restored["mode"] == "delta" and restored["context"] == required
damaged = copy.deepcopy(warm)
damaged["changes"] = {"value": required} # Same output, but an unauthorized alternative change tree.
rejected = restore_context_delivery(damaged, baseline=first, current=current_capture)
assert rejected["reason"] == "automatic_full_recovery" and rejected["context"] == required
index = required["context_details"]["unread_context_index"]
assert index["coverage_confirmed"] is False
assert Path(index["source"]["path"]).name == "projection.json"
Expand Down
2 changes: 2 additions & 0 deletions tests/control_plane_ts/context_delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ test("legacy quota fields are bound by the existing adapter, never caller data",

test("verified file deltas inherit unchanged controls within an epoch without retained-input claims", () => {
const original = {ok: true, goal_id: "goal", controls: {stop: false, owner: "agent"},
context_details: {unread_context_index: {pointers: ["/support"]}},
opaque_control: "Unknown controls also remain available in the exact file. ".repeat(400)};
const baseline = full(original);
const nextScope = {...scope};
Expand All @@ -934,6 +935,7 @@ test("verified file deltas inherit unchanged controls within an epoch without re
assert.equal(delta.continuity_basis, "captured_file");
assert.ok(JSON.stringify(delta).length < JSON.stringify(current).length / 3);
const restored = restoreContextDelivery({delivery: delta, baseline, current: prepareContextDelivery(args)});
assert.equal(restored.mode, "delta");
assert.deepEqual(restored.context, current);
for (const key of ["goal_id", "agent_id", "session_id", "context_epoch", "continuity_generation"]) {
assert.equal(prepareContextDelivery({...args, scope: {...nextScope, [key]: "other"}, baseline, continuity}).mode, "full");
Expand Down
Loading