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
76 changes: 76 additions & 0 deletions docs/architecture/rfcs/human-confirmed-domain-operations-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,79 @@ Before claiming the investment minimum loop, still prove installation,
genuine human approval, bound native consumption, domain preflight and
original-system evidence, accepted result and original-card/audience readback.
The core PR requires owner review and is not self-installed before merge.

### Preparation and canonical return-audience readback

Proposal preparation is not domain execution. An admitted task with supplied
immutable terms may call `prepare` before confirmation or consumption;
`execution_allowed: false` is expected. `context/pending/inspect` also require
no consumption. Reconcile existing preparations first; neither preparation,
waiting nor final-answer prose proves task completion. Domain effects still
require the first successful consumption on the original native tool connection.

For a managed executor, one distinct registered Goal/Agent return audience is
selected automatically. Multiple historical audiences require an explicit host
option, `--codex-operation-source-route-json
'{"host_surface":"codex-app","thread_id":"REGISTERED_THREAD"}'`.
The selector is registered-audience routing, not authentication, a session
replacement or an execution permit; the model cannot retarget it. Managed duplicate
bindings collapse. No registered audience retains the historical null route;
non-managed adapters retain their previous no-source-route projection and do not
invoke this managed resolver. Store
preparation freezes the selected audience in the original confirmation digest.

Source-audience selection and the preparation prompt form a bounded backend
slice, not a completed product delivery. The companion personal-workspace
change below remains a separate first-screen-reviewed delivery in the same plan.
It reads the canonical action list while visible, with
scope-keyed cancellation, a bounded request timeout and no background interval
reads. The existing Manager brief links known-Goal pending operations to their
original drawer; overflow remains reachable through the existing conversation.
The drawer follows the same proposal ID through delivery, confirmation,
consumption, outcome and cancellation. A prepared request without a transport
receipt does not claim that a group card exists. Cancellation is administrative,
not an execution result. Failed readback marks cached state as potentially stale
and provides retry; browsing never confirms or executes an operation.

Companion UI qualification uses isolated canonical backend snapshots through a loopback
HTTP fixture and the packaged EN/ZH desktop/mobile workspace. These synthetic
receipts do not establish genuine approval, live delivery or confirmation-to-host
wakeup latency. Actual click-to-original-session dispatch remains a separate
required acceptance condition using the existing scheduling/session owner.
The backend slice's focused native/normalization tests do not certify that the
companion UI ships in its commit or that source delivery is connected.

### Automatic continuation and original-audience return: remaining work

An authenticated confirmation callback currently records the claim and projects
the managed handoff; it does not launch the original host. Native `report`
commits an outcome and returns on the tool connection, while delivery recovery
updates the original Lark card. Neither is delivery to a selected source
conversation. An `outcome_observed` operation is absent from the pending-work
Inbox, so exhausting that Inbox cannot certify original-audience return.

The same implementation Todo and capability owner retain both gaps. The next
bounded deliveries must prove the following in order; these are planned work,
not qualified features of the preparation/readback slice:

| Delivery | Reused owner and required boundary | Exit evidence |
| --- | --- | --- |
| Confirmation-triggered continuation | The original Turn/session owner and an explicit operator-owned launch binding; the callback passes only an operation locator, never private terms or a new execution grant | Automatic bounded start of the same native session/profile after normal Goal, Todo, quota, lease and expiry checks; duplicate callbacks produce at most one active continuation |
| Restart recovery | The existing Turn journal and kernel single-flight lock; a terminal waiting Turn is not blindly resumed as a new invocation | Crash before/after launch and lost-response tests recover the original attempt; consumed/unknown work reconciles evidence without another submission; stop, session replacement and profile drift deny new launch |
| Source-audience return | The canonical operation/outcome and existing return-delivery semantics, with a qualified adapter for the selected audience | A durable attempt followed by actual readback of the same operation ID, outcome stage and digest at the original audience; unknown delivery is not blindly re-sent; Lark-card and source-conversation receipts remain distinct |
| Original-consumer qualification | The installed, pinned runtime and a real non-financial confirmation on the original test Todo | Actual confirmation, host-start, consumption, outcome and source-delivery timestamps; one consumption; healthy confirmation-to-host-start at most 60 seconds for the initial qualification |

The 60-second threshold is an initial acceptance target, not a proven latency or
a scheduler guarantee. Host-start evidence must come from the native provider's
accepted Turn and matching connection metadata, not just process creation or a
locally stamped `started_at`. The adjacent [delegation lead-wake work](https://github.com/loopx-project/loopx/pull/5304)
targets an originating internal Goal Chat conversation; its planner/recovery
boundary is a reuse candidate, not proof of managed-operation confirmation wake
or external source-audience delivery. Frontend refresh intervals, a later heartbeat, manual
resume and engineering relay messages do not satisfy automatic continuation or
source return. A registered route alone is not a qualified delivery adapter;
an unrelated BotMux binding or attached Desktop identity is not a substitute.
Any new automatic launch configuration must reuse the existing configuration
owner and expose its effective state in the affected product entry points.
Keep the original consumer Todo open until these receipts are observed; do not
convert a cancelled test card or synthetic receipt into genuine approval.
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,58 @@ context 调用,结果均由既有 typed result validator 接受)、规范
不启动平行 resumed 执行者。宣称投研最小闭环前,仍须证明安装、真实用户批准、
绑定原生消费、垂域提交前检查与原系统证据、结果验收及原卡/受众读回。
Core PR 仍须 owner review,不在合并前自行安装。

### 准备提案与规范返回受众读回

准备提案不是领域执行。已准入任务提供不可变条款时,可以在确认和消费前调用
`prepare`,其 `execution_allowed: false` 是正常结果;`context/pending/inspect`
也不要求先消费。先核对已有提案,不重复创建;准备、等待和最终答复文字都不能
证明任务完成。领域副作用仍要求原生工具连接上的首次成功消费回执。

受管执行器只有一个不同的已登记 Goal/Agent 返回受众时自动选择;存在多个历史
受众时,必须由宿主显式传入 `--codex-operation-source-route-json
'{"host_surface":"codex-app","thread_id":"REGISTERED_THREAD"}'`。
这只是已登记的回传受众,不是认证、session 替换或执行许可;模型不能改投。
受管重复绑定会去重;无登记受众保持历史 null 路由,非受管 adapter 保留既有
无 source-route 投影,不调用这条受管解析。准备落盘后,返回受众进入原确认摘要,不可修改。

源受众选择与准备提示构成有界后台切片,不是完整产品交付。下面的个人工作台
配套改动仍在同一计划内单独完成首屏评审后交付。工作台在可见时读取规范
action list,按范围隔离查询并取消旧请求,限制
单次请求时长,不在后台做间隔读取。复用管家简报将已知 Goal 的待确认操作连到
原抽屉,溢出项通过既有对话可达。抽屉按同一提案 ID 跟随投递、确认、消费、
结果和取消。没有投递回执的请求不能声称群卡已存在;取消只是行政终态,不是
执行结果。读回失败明确提示缓存可能过期并允许重试;浏览不确认也不执行操作。

配套 UI 验收通过隔离的规范后端快照、loopback HTTP 夹具和打包页面,覆盖中英文及
桌面/移动端。这些合成回执不证明真实用户批准、真实投递或确认后的即时唤醒。
真实点击到原受管 session 的派发延迟仍是独立的必需验收项,必须复用现有调度
和 session owner。
后台切片的聚焦原生/规范化测试不证明其提交包含配套 UI,也不证明源投递已经接通。

### 自动续接与原受众返回:剩余交付

当前经过认证的确认回调会登记 claim 并投影受管交接,但不会启动原宿主。
原生 `report` 将结果写回规范存储并在工具连接上返回;投递恢复更新原 Lark 卡片。
两者都不等于已向选定的源会话投递。`outcome_observed` 操作不在待办 Inbox 中,
因此读完 Inbox 也不能证明已返回原受众。

这两个缺口继续由同一个实现 Todo 和 capability owner 负责。后续有界交付按以下
顺序验收;这是规划,不是准备/读回切片已经具备或验证的功能:

| 交付 | 复用的权威与必需边界 | 退出证据 |
| --- | --- | --- |
| 确认事件触发续接 | 原 Turn/session owner 与显式的 operator-owned 启动绑定;回调只传操作定位信息,不复制私有条款或新增执行许可 | 经过正常 Goal、Todo、quota、租约及过期核对后自动启动同一个原生 session/profile;重复回调最多产生一个活动续接 |
| 重启恢复 | 既有 Turn journal 与内核 single-flight 锁;不把已终止的等待 Turn 盲目当成新调用续跑 | 覆盖启动前后崩溃与响应丢失,恢复原尝试;已消费/未知操作只核对证据、不重新提交;停止、session 替换及 profile 漂移拒绝新启动 |
| 原受众返回 | 规范操作/结果与既有 return-delivery 语义,选定受众须有合格 adapter | 先登记投递尝试,再真实读回原受众处相同 operation ID、结果阶段及 digest;未知投递不盲目重发;Lark 原卡与源会话回执分开 |
| 原消费者验收 | 已安装且固定的 runtime,以及原测试 Todo 上真实的非金融确认 | 实际确认、宿主启动、消费、结果及源投递时间;一次消费;首轮健康路径从确认到宿主启动不超过 60 秒 |

60 秒是首轮验收目标,不是已证明的延迟或调度保证。宿主启动必须由原生 provider
接受的 Turn 与匹配的连接元数据证明,仅创建进程或本地写入 `started_at` 不够。
相邻的 [delegation lead 唤醒改动](https://github.com/loopx-project/loopx/pull/5304)
面向原发起的内部 Goal Chat 会话;其判定/恢复边界可供复用,不证明受管操作的
确认唤醒或外部源受众已送达。前端刷新间隔、后续 heartbeat、
手动 resume 及工程转述消息都不算自动续接或源返回。路由登记不等于投递 adapter
已合格;无关的 BotMux 绑定或附着 Desktop 身份不能替代。新增自动启动配置必须
复用既有配置 owner,并在受影响的产品入口展示生效状态。看到上述回执前保留原
消费者 Todo 为开放状态,不把已取消测试卡或合成回执当成真实批准。
50 changes: 32 additions & 18 deletions loopx/chat_action_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def _normalize(
"expires_at",
"authorized_principals",
"executor",
"source_route",
},
)
if values.get("schema_version") != "loopx_operation_request_v0":
Expand Down Expand Up @@ -233,26 +234,39 @@ def _normalize(
field: _opaque(raw_executor.get(field), field=f"executor.{field}")
for field in ("extension_id", "protocol", "permission", "revision")
}
source_routes = [
route
for route in (goal.get("coordination") or {}).get(
"thread_agent_bindings", []
managed_source = executor.get("kind") == "managed_turn"
if not managed_source and "source_route" in values:
raise ValueError("source route selection requires a managed executor")
source_route = None
if managed_source:
from .control_plane.effect_runtime import effect_runtime_result
from .thread_agent_binding import (
collect_accepted_bindings,
resolve_thread_agent_binding,
)
if isinstance(route, Mapping) and route.get("agent_id") == agent_id
and all(isinstance(route.get(key), str) and route[key]
for key in ("host_surface", "thread_id"))
]
source_route = (
{
"goal_id": goal_id,
**{
key: source_routes[0][key]
for key in ("agent_id", "host_surface", "thread_id")

selected_route = values.get("source_route")
if isinstance(selected_route, Mapping):
selected_binding = resolve_thread_agent_binding(
goal,
host_surface=selected_route.get("host_surface"),
thread_id=selected_route.get("thread_id"),
)
selected_route = {
**selected_route,
"host_surface": selected_binding["host_surface"],
"thread_id": selected_binding["thread_id"],
}

source_route = effect_runtime_result(
"operation.source_route.resolve",
{
"goal_id": goal_id,
"agent_id": agent_id,
"bindings": collect_accepted_bindings([goal]),
"selected_route": selected_route,
},
}
if len(source_routes) == 1
else None
)
)["source_route"]
expires_at = parse_timestamp(
_text(values.get("expires_at"), field="expires_at", limit=80)
)
Expand Down
5 changes: 4 additions & 1 deletion loopx/cli_commands/turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def handle_turn_command(
strict_goal_admission = goal_admission if goal_admission.enabled else None
if getattr(args, "codex_operation_tools", False) and args.host != "codex-cli":
raise ValueError("--codex-operation-tools requires the codex-cli host")
if getattr(args, "codex_operation_source_route_json", None) is not None and not getattr(args, "codex_operation_tools", False):
raise ValueError("--codex-operation-source-route-json requires --codex-operation-tools")
# Planning and dry-run execution inspect existing admitted intents.
# Only an executing wake may sync inboxes or reserve a calendar window.
turn_start_hook_dispatch = {}
Expand Down Expand Up @@ -1002,7 +1004,8 @@ def run_built_in_host(
)

return run_codex_operation_host(
request, registry_path=registry_path, **options
request, registry_path=registry_path,
source_route=getattr(args, "codex_operation_source_route_json", None), **options
)
return run_codex_cli_host(request, **options)

Expand Down
5 changes: 5 additions & 0 deletions loopx/cli_commands/turn_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ def register_turn_commands(
action="store_true",
help="Opt in to the owned app-server operation transport for this admitted codex-cli Turn. Reuses the original Todo/session; does not authenticate an attached Desktop or grant domain effects.",
)
run_once.add_argument(
"--codex-operation-source-route-json",
type=json.loads,
help="Registered return audience {host_surface,thread_id} for operation proposals. Required when this Agent has several source routes; not executor authentication or execution permission.",
)
run_once.add_argument(
"--codex-reasoning-effort",
choices=list(REASONING_EFFORTS),
Expand Down
3 changes: 2 additions & 1 deletion loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {manageNewGoalStorage} from "./coordination/local_authority_defaults.ts";
import {deriveAgentOperationActor, managedOperationBindingCurrent, normalizeAgentOperationExecutor, planAgentOperationHandoff, projectAgentOperationInbox, projectManagedOperationTransport} from "./work_items/operation_agent_handoff.ts";
import {deriveAgentOperationActor, managedOperationBindingCurrent, normalizeAgentOperationExecutor, planAgentOperationHandoff, projectAgentOperationInbox, projectManagedOperationTransport, resolveOperationSourceRoute} from "./work_items/operation_agent_handoff.ts";
import {projectDecisionNotice} from "./presentation/decision_notice.ts";
import {normalizeResearchObservation, validateResearchAttribution, projectResearchFrontier} from "./capabilities/explore_research.ts";
import {projectTodoSummary} from "./todos/summary_projection.ts";
Expand Down Expand Up @@ -632,6 +632,7 @@ export function createEffectRuntimeHandlers(
["presentation.action_review_plan.compile", (params) =>
compileActionReviewPlan(params.proposal)],
["operation.agent_executor.normalize", normalizeAgentOperationExecutor],
["operation.source_route.resolve", resolveOperationSourceRoute],
["operation.managed_binding.current", managedOperationBindingCurrent],
["operation.managed_transport.project", projectManagedOperationTransport],
["operation.agent_handoff.actor", deriveAgentOperationActor],
Expand Down
Loading
Loading