Skip to content
Open
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
@@ -0,0 +1,52 @@
# Executed settlement and receipt source boundaries

Measured from `36134771355f05c9bcc5657ce739bb86426383dc`, for #4447 Stage 2a.
The scope revision in #4789 remains a proposal. This entry changes neither RFC
acceptance nor the F1/F2 production domain: **7/26**, with these four entries
still outside it. Source treatment and scanner enrollment are separate claims.

- **Settlement envelopes (`settlement_step_kind`, `settlement_failure_kind`).**
`turn_driver/settlement.ts::reduceTurnSettlementTransaction` calls the shared
`effect_program.ts` builders; its `result.receipts[].step_kind` and
`result.failure.{kind,step_kind}` cross `effect_runtime_result` into
`effect_program.py::decode_settlement_result_payload`. The witness executes
the real reducer directly and through the bridge: committed replay produces
all four steps without dispatch/checkpoint; identity/prefix rejection,
provider refusal, terminal refusal and unknown prepared outcome produce
eight failure kinds. Real file readback adds `writeback_missing`.
- **External input is a separate obligation.** The live
`settlement.bind_gate` handler runs `settlementResultInput` before its builder.
Injected envelopes exercise admission of four step and eleven failure values,
plus unknown/null/numeric rejection in each step/failure slot through both TS
and Python decoders. These are input witnesses, not eleven producer witnesses.
`permission_denied` has task-lease producers in `task_lease_acquire.ts` and
`task_lease_lifecycle.ts`, but this batch does not execute them. `cancelled`
remains decoder-admitted without an identified producing branch; it is not
newly classified as compatibility-only. Both production obligations stay open.
- **Receipt phases (`receipt_bound_monitor_phase`, `receipt_bound_replay_phase`).**
`quota/settlement_readback.ts::readQuotaSettlement` derives facts from isolated
synthetic receipt files and calls `quota/settlement_phase.ts`; Python
`read_heartbeat_settlement` decodes the results. Exact monitor commit, absent
or wrong commit, completion/writeback/spend prefixes, repeated readback and
conflicting identity exercise actual callers. Monitor emits `poll_due` or
`settled` even without spend; `settlement_pending` remains accepted by the
Python work-lane consumer only as compatibility evidence. The legacy monitor
effect id and terminal-to-replay adapter are retained. Removing them requires
separate historical-reader/caller migration evidence. Replay's three values
are produced; autonomous-replan binding is additionally exercised through the
phase bridge, not a full replan receipt transaction.

Run `uv run --extra test python -m pytest tests/architecture/test_settlement_receipt_source_boundaries.py`:
**62 passed** using Python 3.12.3 and qualified Node 22.22.3. With the existing
binding witness, settlement-driver and quota-settlement tests: **142 passed**.
The two native TS settlement/readback suites: **58 passed**, no skips.
Docs governance, focused lint/type checks and semantic drift smoke pass; the
latter still reports F1/F2 **7/26** and 19 unverified cross-runtime entries.
The thin `scripts/settlement_receipt_source_witness.mts` imports shipped owners;
it introduces no runtime rule, registry metadata or global vocabulary.

Limits: synthetic readback is not proof of durable writers, all call sites, live
CLI/backend qualification or F6 history compatibility. The existing budget-text
failure classifier is characterized, not repaired. No production refactor or
frontend/Lark/CLI change is included; the next owner action is evidence review
and the named missing producer witnesses, not automatic source closure.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Settlement 与 receipt 的可执行来源边界

基线 `36134771355f05c9bcc5657ce739bb86426383dc`,对应 #4447 Stage 2a。
#4789 的范围修订仍是提案。本条不改变 RFC 验收或 F1/F2 生产域:仍为
**7/26**,本组四项仍在域外。来源处理与生产扫描纳入是不同结论。

- **Settlement envelope(`settlement_step_kind`、`settlement_failure_kind`)。**
`turn_driver/settlement.ts::reduceTurnSettlementTransaction` 调用共享
`effect_program.ts` builder;`result.receipts[].step_kind` 与
`result.failure.{kind,step_kind}` 经 `effect_runtime_result` 到达
`effect_program.py::decode_settlement_result_payload`。Witness 直接执行真实
reducer 并经 bridge 复核:已提交重放产生四种 step,不再 dispatch/checkpoint;
identity/prefix 拒绝、provider 拒绝、terminal 拒绝及 prepared outcome unknown
产生八种 failure。真实文件读回另证明 `writeback_missing`。
- **外部输入是独立义务。** 实际 `settlement.bind_gate` handler 先执行
`settlementResultInput`。注入 envelope 覆盖四种 step、十一种 failure 的接纳,
以及各 step/failure 槽位对 unknown/null/数字的 TS 与 Python 解码拒绝。这是
输入证据,不是十一种生产证据。`permission_denied` 在
`task_lease_acquire.ts`、`task_lease_lifecycle.ts` 有生产点,本组尚未执行;
`cancelled` 仅证明解码接纳,尚未识别生产分支,不将其新归类为 compatibility-only。
两项生产义务继续保留。
- **Receipt phase(`receipt_bound_monitor_phase`、`receipt_bound_replay_phase`)。**
`quota/settlement_readback.ts::readQuotaSettlement` 从隔离合成 receipt 文件提取
事实,调用 `quota/settlement_phase.ts`,Python `read_heartbeat_settlement`
解码结果。精确 monitor commit、缺失或错误 commit、completion/writeback/spend
前缀、重复读回及冲突 identity 均经过实际调用点。Monitor 无需 spend 即产生
`poll_due` 或 `settled`;`settlement_pending` 仅以 Python work-lane 消费者的
兼容接纳证明保留。旧 monitor effect id 与 terminal-to-replay adapter 继续保留;
删除需要独立的历史 reader/caller 迁移证据。Replay 三种值均有产生证据;
autonomous-replan binding 另经 phase bridge 验证,未执行完整 replan receipt 交易。

运行 `uv run --extra test python -m pytest tests/architecture/test_settlement_receipt_source_boundaries.py`:
Python 3.12.3、合格 Node 22.22.3 下 **62 passed**;连同已有 binding witness、
settlement-driver、quota-settlement 测试共 **142 passed**。两组原生 TS
settlement/readback 测试 **58 passed**,无跳过。文档治理、焦点 lint/type 检查和
语义漂移 smoke 通过;后者仍报告 F1/F2 **7/26**、19 项跨运行时未验证。薄脚本
`scripts/settlement_receipt_source_witness.mts` 导入现有 owner,不新增 runtime
规则、registry 元数据或全局词表。

边界:合成读回不证明 durable writer、所有调用点、真实 CLI/backend 资格或 F6
历史兼容。现有 budget 文本分类只被刻画,未被修复。未做生产重构,也未改变
frontend/Lark/CLI;后续由相应 owner 评审证据并补已点名的生产 witness,不自动
宣告来源全部关闭。
31 changes: 11 additions & 20 deletions loopx/chat_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,18 +1055,21 @@ def _action_preview(self) -> None:
status=201,
)

def _action_not_found(self) -> None:
self._send_error(
"typed Chat action proposal was not found",
status=404,
error_code="action_not_found",
)

def _action_snapshot(self, proposal_id: str) -> None:
try:
proposal = self.server.action_service.load(proposal_id)
except ValueError as exc:
self._send_error(str(exc), status=400, error_code="invalid_proposal_id")
return
if proposal is None:
self._send_error(
"typed Chat action proposal was not found",
status=404,
error_code="action_not_found",
)
self._action_not_found()
return
self._send_json(
{
Expand Down Expand Up @@ -1107,11 +1110,7 @@ def _action_cancel(self, proposal_id: str) -> None:
raise ValueError("action cancel request must be empty")
proposal = self.server.action_service.cancel(proposal_id)
except KeyError:
self._send_error(
"typed Chat action proposal was not found",
status=404,
error_code="action_not_found",
)
self._action_not_found()
return
except ActionConflictError as exc:
self._send_error(str(exc), status=409, error_code="action_conflict")
Expand Down Expand Up @@ -1144,11 +1143,7 @@ def _action_transition(self, proposal_id: str, transition: str) -> None:
else:
raise ValueError("unsupported action transition")
except KeyError:
self._send_error(
"typed Chat action proposal was not found",
status=404,
error_code="action_not_found",
)
self._action_not_found()
return
except ActionConflictError as exc:
self._send_error(str(exc), status=409, error_code="action_conflict")
Expand Down Expand Up @@ -1190,11 +1185,7 @@ def _action_apply(self, proposal_id: str) -> None:
)
return
except KeyError:
self._send_error(
"typed Chat action proposal was not found",
status=404,
error_code="action_not_found",
)
self._action_not_found()
return
except ActionConflictError as exc:
self._send_error(str(exc), status=409, error_code="action_conflict")
Expand Down
49 changes: 3 additions & 46 deletions loopx/extensions/lark/goal_topic_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
IngressMode,
ReplyMode,
_routing_value,
_connection_routing_modes,
decide_lark_topic_route_event,
)
from .presentation.kanban import (
Expand Down Expand Up @@ -1035,29 +1036,7 @@ def list_lark_connections(
)
connector_status: dict[str, Any] | None = None
try:
capture_scope = _routing_value(
CaptureScope,
routing.get("capture_scope")
or (
"configured_chat_all"
if routing.get("incoming_mode") == "all"
else "addressed_only"
),
default=CaptureScope.ADDRESSED_ONLY.value,
field="capture_scope",
)
ingress_mode = _routing_value(
IngressMode,
routing.get("ingress_mode"),
default=IngressMode.DIRECT_SESSION.value,
field="ingress_mode",
)
reply_mode = _routing_value(
ReplyMode,
routing.get("reply_mode"),
default=ReplyMode.TOPIC_REPLY.value,
field="reply_mode",
)
capture_scope, ingress_mode, reply_mode = _connection_routing_modes(routing)
raw_connector = binding.get("connector")
if raw_connector is not None:
if not isinstance(raw_connector, Mapping):
Expand Down Expand Up @@ -1216,29 +1195,7 @@ def decide_lark_topic_event(
else {}
)
try:
capture_scope = _routing_value(
CaptureScope,
routing.get("capture_scope")
or (
"configured_chat_all"
if routing.get("incoming_mode") == "all"
else "addressed_only"
),
default=CaptureScope.ADDRESSED_ONLY.value,
field="capture_scope",
)
ingress_mode = _routing_value(
IngressMode,
routing.get("ingress_mode"),
default=IngressMode.DIRECT_SESSION.value,
field="ingress_mode",
)
reply_mode = _routing_value(
ReplyMode,
routing.get("reply_mode"),
default=ReplyMode.TOPIC_REPLY.value,
field="reply_mode",
)
capture_scope, ingress_mode, reply_mode = _connection_routing_modes(routing)
connector = binding.get("connector")
if connector is not None:
if not isinstance(connector, Mapping):
Expand Down
31 changes: 31 additions & 0 deletions loopx/extensions/lark/goal_topic_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,37 @@ def _routing_value(
raise ValueError(f"{field} must be one of: {allowed}") from exc


def _connection_routing_modes(
routing: Mapping[str, Any],
) -> tuple[str, str, str]:
"""Normalize persisted modes for both connection readback and event routing."""

capture_scope = _routing_value(
CaptureScope,
routing.get("capture_scope")
or (
"configured_chat_all"
if routing.get("incoming_mode") == "all"
else "addressed_only"
),
default=CaptureScope.ADDRESSED_ONLY.value,
field="capture_scope",
)
ingress_mode = _routing_value(
IngressMode,
routing.get("ingress_mode"),
default=IngressMode.DIRECT_SESSION.value,
field="ingress_mode",
)
reply_mode = _routing_value(
ReplyMode,
routing.get("reply_mode"),
default=ReplyMode.TOPIC_REPLY.value,
field="reply_mode",
)
return capture_scope, ingress_mode, reply_mode


def _normalize_mention_name(name: str) -> str:
cleaned = str(name or "").strip()
if cleaned.startswith("@"):
Expand Down
19 changes: 19 additions & 0 deletions scripts/settlement_receipt_source_witness.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Bounded source witness: execute shipped owners, without defining vocabulary
// values, classifying sources, or enrolling them in the production scanner.
import { readFileSync } from "node:fs";
import { reduceTurnSettlementTransaction } from "../loopx/control_plane/turn_driver/settlement.ts";
import { readQuotaSettlement } from "../loopx/control_plane/quota/settlement_readback.ts";

const request = JSON.parse(readFileSync(0, "utf8"));
let result: unknown;
switch (request.operation) {
case "turn":
result = reduceTurnSettlementTransaction(request.input);
break;
case "readback":
result = await readQuotaSettlement(request.input);
break;
default:
throw new Error("unsupported settlement source witness operation");
}
process.stdout.write(JSON.stringify(result));
Loading
Loading