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
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,21 @@ the existing context-delivery owner. C1 must converge the remaining composition
and consumer assumptions, rather than introduce another capture service or
rewrite already shared delta/native-resume logic.

Journal persistence is an independent source-fidelity prerequisite. The proposed
Python adapter reuses context delivery's sparse number-token collector; both
TypeScript consumers reuse one source-number validator. Journals remain JSON
objects, with metadata only for integral floats, signed zero and unsafe integers.
The journal owner restores these numbers before atomic publication and retains
their distinctions in replay and immutable-plan comparisons. Ordinary native
objects retain their encoding; historical receipts are not rewritten.

The interim whole-JSON-string carrier failed the existing 2 MiB request boundary
for a synthetic 1.55-million-character quoted body accepted by the object path.
Keep that negative result: qualify the sparse replacement against the same
workload and limit, including number-dense overhead. Neither this prerequisite
nor a component byte saving qualifies the unified writer/consumer cutover or
long-horizon net benefit.

| Current source / obligation | Present envelope treatment | Required C1 disposition |
| --- | --- | --- |
| `interaction_contract.agent_channel.work_context` and pending required reads | `work_context` plus `required_reads`; identical inline material can be referenced | One complete requirement/evidence source with resolvable references; preserve pending reads and their completion conditions |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ Quota capture 与 Turn journal capture 都调用
context-delivery owner。C1 应收敛剩余的组合逻辑与消费者假设,不另建 capture service,
也不重写已经共享的差分与 native-resume 逻辑。

Journal 持久化是独立的来源保真前置。本次提议由 Python adapter 复用 context
delivery 的稀疏数值 token 收集器;两个 TypeScript 消费者复用同一来源数值校验器。
Journal 继续传递 JSON 对象,仅为整值浮点、带符号零和非安全整数增加元数据。
Journal owner 在原子发布前恢复这些数值,并在重放与不可变 plan 比较中保留其
区别。普通原生对象保留已有编码,不改写历史回执。

中间的完整 JSON 字符串方案,在合成的 155 万字符带引号正文上超过既有 2 MiB
请求边界;原对象路径能够接收同一负载。保留这一负面结果:稀疏替代须在相同
负载与上限下比较,包括数值密集时的元数据开销。该前置或组件字节减少,都不
构成统一 writer/consumer 切换或长程净收益的资格。

| 当前来源 / 义务 | 当前 envelope 处理 | C1 必须保留的归属与语义 |
| --- | --- | --- |
| `interaction_contract.agent_channel.work_context` 与未完成必读 | `work_context` 加 `required_reads`;相同内联材料可改用引用 | 一份完整要求/证据来源与可解析引用;保留未完成必读及其完成条件 |
Expand Down
18 changes: 18 additions & 0 deletions docs/reference/protocols/loopx-turn-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,24 @@ completed effects remain proved; other effects are unknown, not `false`. The
writer's empty-map rejection is retained: no pending intent uses an absent field;
committed history is carried by completed checkpoints, not retained intents.

Python-origin journal snapshots remain JSON objects through that same TypeScript
writer. They reuse context delivery's sparse numeric IO metadata and validator
to preserve integral floats, signed zero and unsafe integers before atomic
publication. Replay and immutable-plan comparison distinguish these source
numbers; refusals retain the original file. Ordinary objects without metadata
retain their encoding and operation identity; historical receipts are not
rewritten. This remains one RPC under the existing 2 MiB request limit and avoids
double escaping evidence text. Metadata still costs bytes for number-dense
sources. Source fidelity and transport checks do not establish latency or
long-horizon benefit.

Python 来源的 journal 快照仍经同一 TypeScript writer 传递 JSON 对象,复用
context delivery 的稀疏数值 IO 元数据与校验器,在原子发布前保留整值浮点、带
符号零和非安全整数。重放与不可变 plan 比较区分这些来源数值;拒绝时保留原
文件。无元数据的普通对象保留已有编码与操作身份,不改写历史回执。仍为一次
RPC,沿用 2 MiB 请求上限,避免证据正文的双重转义;数值密集来源仍有元数据
字节成本。来源保真与传输检查不代表延迟或长程收益已得到验证。

If executing `run-once` raises unexpectedly, its error response retains the
original error and resume key, marks uncertain current-invocation `effects` as
`null`, and adds `journal_observation` from this same read-only TS owner. Its
Expand Down
26 changes: 26 additions & 0 deletions loopx/control_plane/effect_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import hashlib
import json
import math
import os
import secrets
import socket
Expand Down Expand Up @@ -86,6 +87,31 @@
_RuntimeSourceSnapshot = tuple[tuple[str, int, int, int], ...]


def source_number_tokens(value: object) -> list[list[Any]]:
"""Carry numeric IO spellings that a JavaScript number cannot distinguish.

This is transport metadata, not a hash codec or a projection policy.
Fractional doubles and safe integers keep their existing object transport.
"""
tokens: list[list[Any]] = []

def visit(item: object, path: list[str | int]) -> None:
if isinstance(item, float) and not math.isfinite(item):
raise ValueError("Context source numbers must be finite")
if (isinstance(item, float) and item.is_integer()
or type(item) is int and abs(item) > 2**53 - 1):
tokens.append([path, json.dumps(item, allow_nan=False)])
elif isinstance(item, Mapping):
for key, child in item.items():
visit(child, [*path, key])
elif isinstance(item, (list, tuple)):
for index, child in enumerate(item):
visit(child, [*path, index])

visit(value, [])
return tokens


class _RuntimeSourceChanged(RuntimeError):
def __init__(self, snapshot: _RuntimeSourceSnapshot) -> None:
super().__init__("runtime source changed while hashing")
Expand Down
28 changes: 2 additions & 26 deletions loopx/control_plane/quota/diagnostic_projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
* decision. This projection grants no admission or execution authority.
*/
import type {JsonObject} from "../effect_program.ts";
import {jsonObject} from "../runtime_decode.ts";
import {jsonObject, sourceNumberTokens} from "../runtime_decode.ts";
import {TODO_CANONICAL_READ_RECORD_FIELDS} from "../coordination/coordination_state_contract.ts";
import {canonicalAuthorityJson, authorityUnicodeCompare} from "../coordination/authority_store_codec.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";

type ProjectionPath = (string | number)[];
type ProjectionChange = {path: ProjectionPath; value: unknown} |
Expand All @@ -33,30 +32,7 @@ function contextObjectPaths(source: JsonObject): WeakMap<object, ProjectionPath>
export function contextSourceIdentity(source: JsonObject, supplied?: unknown):
(value: unknown, path: ProjectionPath) => string {
if (supplied === undefined) return value => JSON.stringify(canonicalAuthorityJson(value));
if (!Array.isArray(supplied)) throw new EffectRuntimeRequestError("context source number tokens must be an array");
const tokens: {path: ProjectionPath; token: string}[] = [], seen = new Set<string>();
for (const entry of supplied) {
if (!Array.isArray(entry) || entry.length !== 2 || !Array.isArray(entry[0]) || !entry[0].length ||
typeof entry[1] !== "string" || !/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(entry[1])) {
throw new EffectRuntimeRequestError("invalid context source number token");
}
const path: ProjectionPath = entry[0], token: string = entry[1];
let value: unknown = source;
for (const key of path) {
if (value === null || typeof value !== "object" ||
!(Array.isArray(value) ? typeof key === "number" && Number.isSafeInteger(key) && key >= 0
: typeof key === "string") || !Object.hasOwn(value, key)) {
throw new EffectRuntimeRequestError("context source number token path is unavailable");
}
value = (value as Record<string | number, unknown>)[key];
}
const key = JSON.stringify(path);
if (seen.has(key) || typeof value !== "number" || !Number.isFinite(value) || !Object.is(JSON.parse(token), value)) {
throw new EffectRuntimeRequestError("context source number token differs from input");
}
seen.add(key);
tokens.push({path, token});
}
const tokens = sourceNumberTokens(source, supplied);
const paths = contextObjectPaths(source);
return (value, path) => {
const original = value !== null && typeof value === "object" ? paths.get(value) : undefined;
Expand Down
29 changes: 2 additions & 27 deletions loopx/control_plane/quota/turn_envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
from __future__ import annotations

import json
import math
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any

from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result
from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result, source_number_tokens
from ..scheduler.execution_context import (
SchedulerExecutionContextResolution,
render_scheduler_execution_args,
Expand Down Expand Up @@ -57,7 +56,7 @@ def _turn_envelope_result(

request = {"operation": operation, **params}
if operation in {"context_delivery", "context_recovery"}:
tokens = _source_number_tokens(params)
tokens = source_number_tokens(params)
if tokens:
request["source_number_tokens"] = tokens
try:
Expand All @@ -74,30 +73,6 @@ def _turn_envelope_result(
return dict(result)


def _source_number_tokens(value: object) -> list[list[Any]]:
"""Transport sparse numeric source identity; no carrier or sharing policy."""
tokens: list[list[Any]] = []

def visit(item: object, path: list[str | int]) -> None:
if isinstance(item, float) and not math.isfinite(item):
raise ValueError("Context source numbers must be finite")
# Fractional doubles retain their identity through JSON/JavaScript.
# Only integral floats (including signed zero) and unsafe integers
# can become indistinguishable from a different Python source number.
if (isinstance(item, float) and item.is_integer()
or type(item) is int and abs(item) > 2**53 - 1):
tokens.append([path, json.dumps(item, allow_nan=False)])
elif isinstance(item, Mapping):
for key, child in item.items():
visit(child, [*path, key])
elif isinstance(item, (list, tuple)):
for index, child in enumerate(item):
visit(child, [*path, index])

visit(value, [])
return tokens


def assert_turn_envelope_consumer_version(envelope: Mapping[str, Any]) -> None:
"""Use the typed wire-version gate before any Host consumes this packet."""
_turn_envelope_result("consumer_version", envelope=dict(envelope))
Expand Down
33 changes: 33 additions & 0 deletions loopx/control_plane/runtime_decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@ export function requireJsonObject(value: unknown, label: string): JsonObject {
return result;
}

/** Sparse IO spellings, validated against the already decoded source.
* Shared by context identity and durable journal publication; no carrier,
* authority or projection decision is made here. */
export function sourceNumberTokens(source: JsonObject, supplied?: unknown):
{path: (string | number)[]; token: string}[] {
if (supplied === undefined) return [];
if (!Array.isArray(supplied)) throw new EffectRuntimeRequestError("context source number tokens must be an array");
const tokens: {path: (string | number)[]; token: string}[] = [], seen = new Set<string>();
for (const entry of supplied) {
if (!Array.isArray(entry) || entry.length !== 2 || !Array.isArray(entry[0]) || !entry[0].length ||
typeof entry[1] !== "string" || !/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(entry[1])) {
throw new EffectRuntimeRequestError("invalid context source number token");
}
const path: (string | number)[] = entry[0], token: string = entry[1];
let value: unknown = source;
for (const key of path) {
if (value === null || typeof value !== "object" ||
!(Array.isArray(value) ? typeof key === "number" && Number.isSafeInteger(key) && key >= 0
: typeof key === "string") || !Object.hasOwn(value, key)) {
throw new EffectRuntimeRequestError("context source number token path is unavailable");
}
value = (value as Record<string | number, unknown>)[key];
}
const key = JSON.stringify(path);
if (seen.has(key) || typeof value !== "number" || !Number.isFinite(value) || !Object.is(JSON.parse(token), value)) {
throw new EffectRuntimeRequestError("context source number token differs from input");
}
seen.add(key);
tokens.push({path, token});
}
return tokens;
}

export function requireBoolean(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new EffectRuntimeRequestError(`${label} must be a boolean`);
Expand Down
Loading
Loading