跨平台资料拉取:自主触发候选 + 复用深挖任务委托执行核(可抢占) #76

Merged
Yushu merged 2 commits from feature/71-cross-platform-pull into main 2026-07-27 04:37:42 +00:00
Member

Closes #71

What

用户明确同意后,机器人自主、周期性地了解对方在其它平台上的动态记入画像,不必然触发一条打扰用户的消息。ADR-0027 落地,长尾记忆/感知增强包(issue #66)最后一片。

实现要点

  • 触发候选复用 spontaneous_goal.familiar_reconnect_hint 精确形状:纯函数 cross_platform_pull_hint 达标才产出提示,注入 Drive Tick 既有 light context 同一挂载位置,不新增独立评估通道。产出的只是「多一层理由」,主模型自己决定要不要真去查。
  • 执行核直接复用 run_delegated_task 本体、不加来源标记:它是纯函数式执行核(只吃 description/llm_client/get_tools/max_rounds、零状态、不认识调用方),治理差异全在调用方。subagent.py 一行未改。
  • 新起专用工具而非让模型调 delegate_task(AC 把这个选择留实现期按成本定):那条路径连着登记表 + Callback 送回 + 履约不可抢占治理,而本机制要的恰恰是不同的治理。走同一工具就得在 _run_delegated_task 里按来源分叉、改既有 schema/登记表,既污染既有契约又更贵。工具无参 —— 查谁由 core 按当前 Drive Tick 对象定,模型无法指定别人。
  • 可抢占(与深挖任务委托的关键差异)分两段:起点「真人正活跃就搁置」恒生效;执行中 cancel 由 arise_cross_platform_pull_preemptible(默认真)控制。竞速用 asyncio.wait 而非在拉取内部埋检查点,就是为了不给被复用的既有执行核插桩。
  • per-user 显式同意(比习得贴纸的 per-chat 开关更进一步),本片连命令一起做,刻意不加权限校验 —— 同意主体是内容所有者本人,与 chat 管理权限是两个不同授权维度。撤销不清 last_pulled_at,免得「撤销再同意」变成绕过间隔节流的手段。
  • 产出写既有画像治理(sensitive 硬编码为真 + 落库前过人设漂移守护否决闸)。

Review

两轴都抓到实质缺陷,四条必修:

  1. 真 bug:注入的回调是同步 def,handler 里却 await 它的返回值 → TypeError,模型一调这个工具整轮就炸、拉取永不发生。根因是这条路零测试覆盖(我测了候选/治理/命令,唯独没测「模型真的调这个工具」)。已改 async 并补三条覆盖该路径的测试。
  2. 同意键错配:写入用 event.get_user_id()(原始适配器 id),读取用 chat_id.removesuffix(".p")(host 经 ingress port 映射后)—— 在做 id 归一化的 host 上会写进一个永远查不到的键,或命中另一个人的 id。已收敛到纯函数 consent_subject_of,两侧同源。
  3. 隐私外送_known_platform_facts 把该用户全部画像事实(含住址/健康/财务)拼进任务描述交给 host 抓取工具 —— 与本机制立意直接相反。已改为只带非敏感事实。
  4. 同一轮多次调用起并发子智能体:候选每轮只算一次,但模型在同一轮工具循环里可以调多次。已加 per-user in-flight 闸。

另外修了:竞速的 try/finally 收口(原先「外层任务被 cancel」时两个子任务都悬空);同意命令加 to_me()COMMAND_START 含空串时群里有人引用那句话就会被当成授权);删死代码;两个阈值进 config。

关于可抢占开关(已定论)

Spec 轴认为 arise_cross_platform_pull_preemptible 属 scope creep:AC4「不复用履约不可抢占治理」是无条件的,而这个开关设 False 时执行中确实变成不可抢占。

已与维护者确认:保留这个开关,并已在 ADR-0027 里注明它是「部署侧成本取舍」而非「允许退回 ADR-0020 的治理复用」。 区分两者的判据是起点那道闸有没有恒生效——「真人正活跃就不起这个任务」不可配、恒生效,而 delegate_task 连起点都不看活跃度(用户已经在等,本来就该立刻开始)。所以关掉开关只意味着「已经开始的这一次不半途丢弃已付的 LLM 成本」,不意味着本机制获得了「履约」地位。

ADR-0027 三处已更新(决策节新增该条、「拒绝了」节点明拒的是治理复用而非这个开关、「取代/关系」节加限定)。文档改动在 docs 分支本地 commit d76e8c4,按既定规则不推送。

已知范围边界

sensitive 过滤是粗筛:可能漏掉一个被 delta 判成敏感的小众平台账号,那条就查不了。刻意选的方向 —— 漏查一次的成本远低于把一个人的住址送给外部抓取工具。

Tests

全仓库 1071 个测试通过(新增 44)。变异复核五处守卫:同意闸挂 5 条、起点闸挂 2 条、in-flight 闸挂 1 条、敏感过滤挂 3 条、可抢占竞速关掉后测试挂死(据此给那条测试加了 wait_for 超时)。

Closes #71 ## What 用户明确同意后,机器人自主、周期性地了解对方在其它平台上的动态记入画像,不必然触发一条打扰用户的消息。ADR-0027 落地,长尾记忆/感知增强包(issue #66)最后一片。 ## 实现要点 - **触发候选**复用 `spontaneous_goal.familiar_reconnect_hint` 精确形状:纯函数 `cross_platform_pull_hint` 达标才产出提示,注入 Drive Tick 既有 light context 同一挂载位置,不新增独立评估通道。产出的只是「多一层理由」,主模型自己决定要不要真去查。 - **执行核直接复用 `run_delegated_task` 本体、不加来源标记**:它是纯函数式执行核(只吃 description/llm_client/get_tools/max_rounds、零状态、不认识调用方),治理差异全在调用方。`subagent.py` 一行未改。 - **新起专用工具而非让模型调 `delegate_task`**(AC 把这个选择留实现期按成本定):那条路径连着登记表 + Callback 送回 + 履约不可抢占治理,而本机制要的恰恰是不同的治理。走同一工具就得在 `_run_delegated_task` 里按来源分叉、改既有 schema/登记表,既污染既有契约又更贵。工具无参 —— 查谁由 core 按当前 Drive Tick 对象定,模型无法指定别人。 - **可抢占(与深挖任务委托的关键差异)分两段**:起点「真人正活跃就搁置」恒生效;执行中 cancel 由 `arise_cross_platform_pull_preemptible`(默认真)控制。竞速用 `asyncio.wait` 而非在拉取内部埋检查点,就是为了不给被复用的既有执行核插桩。 - **per-user 显式同意**(比习得贴纸的 per-chat 开关更进一步),本片连命令一起做,刻意不加权限校验 —— 同意主体是内容所有者本人,与 chat 管理权限是两个不同授权维度。撤销不清 `last_pulled_at`,免得「撤销再同意」变成绕过间隔节流的手段。 - 产出写既有画像治理(`sensitive` 硬编码为真 + 落库前过人设漂移守护否决闸)。 ## Review 两轴都抓到实质缺陷,四条必修: 1. **真 bug**:注入的回调是同步 `def`,handler 里却 `await` 它的返回值 → `TypeError`,模型一调这个工具整轮就炸、拉取永不发生。根因是这条路**零测试覆盖**(我测了候选/治理/命令,唯独没测「模型真的调这个工具」)。已改 async 并补三条覆盖该路径的测试。 2. **同意键错配**:写入用 `event.get_user_id()`(原始适配器 id),读取用 `chat_id.removesuffix(".p")`(host 经 ingress port 映射后)—— 在做 id 归一化的 host 上会写进一个永远查不到的键,或命中另一个人的 id。已收敛到纯函数 `consent_subject_of`,两侧同源。 3. **隐私外送**:`_known_platform_facts` 把该用户**全部**画像事实(含住址/健康/财务)拼进任务描述交给 host 抓取工具 —— 与本机制立意直接相反。已改为只带非敏感事实。 4. **同一轮多次调用起并发子智能体**:候选每轮只算一次,但模型在同一轮工具循环里可以调多次。已加 per-user in-flight 闸。 另外修了:竞速的 `try/finally` 收口(原先「外层任务被 cancel」时两个子任务都悬空);同意命令加 `to_me()`(`COMMAND_START` 含空串时群里有人引用那句话就会被当成授权);删死代码;两个阈值进 config。 ## 关于可抢占开关(已定论) Spec 轴认为 `arise_cross_platform_pull_preemptible` 属 scope creep:AC4「不复用履约不可抢占治理」是无条件的,而这个开关设 `False` 时执行中确实变成不可抢占。 **已与维护者确认:保留这个开关,并已在 ADR-0027 里注明它是「部署侧成本取舍」而非「允许退回 ADR-0020 的治理复用」。** 区分两者的判据是**起点那道闸有没有恒生效**——「真人正活跃就不起这个任务」不可配、恒生效,而 `delegate_task` 连起点都不看活跃度(用户已经在等,本来就该立刻开始)。所以关掉开关只意味着「已经开始的这一次不半途丢弃已付的 LLM 成本」,不意味着本机制获得了「履约」地位。 ADR-0027 三处已更新(决策节新增该条、「拒绝了」节点明拒的是治理复用而非这个开关、「取代/关系」节加限定)。文档改动在 `docs` 分支本地 commit `d76e8c4`,按既定规则不推送。 ## 已知范围边界 `sensitive` 过滤是粗筛:可能漏掉一个被 `delta` 判成敏感的小众平台账号,那条就查不了。刻意选的方向 —— 漏查一次的成本远低于把一个人的住址送给外部抓取工具。 ## Tests 全仓库 1071 个测试通过(新增 44)。变异复核五处守卫:同意闸挂 5 条、起点闸挂 2 条、in-flight 闸挂 1 条、敏感过滤挂 3 条、可抢占竞速关掉后测试挂死(据此给那条测试加了 `wait_for` 超时)。
用户明确同意后,机器人周期性了解对方在其它平台上的动态记入画像,不必然触发
一条打扰用户的消息。ADR-0027 落地,本增强包最后一片。

**触发候选**复用 `spontaneous_goal.familiar_reconnect_hint` 精确形状:纯函数
`cross_platform_pull_hint` 达标才产出提示,注入 Drive Tick 既有 light context
同一挂载位置,不新增独立评估通道。产出的只是"多一层理由",主模型自己决定要
不要真去查(ADR-0027"要不要去查本身也是一次主动性决策")。

**执行核直接复用 `run_delegated_task` 本体,不加来源标记**:它是纯函数式执行核
(只吃 description/llm_client/get_tools/max_rounds、零状态、不认识调用方),
治理差异全在调用方。既有执行核代码与行为契约零改动。

**新起专用工具而非让模型调 delegate_task**(AC 把这个选择留给实现期按成本定):
delegate_task 那条路径连着登记表 + Callback 送回 + 履约不可抢占治理,而本机制
要的恰恰是不同的治理。走同一工具就得在 `_run_delegated_task` 里按来源分叉、改
既有 schema/登记表,既污染既有契约又更贵。工具无参——查谁由 core 按当前 Drive
Tick 对象定,模型无法指定别人,避免绕过同意主体。

**可抢占(与深挖任务委托的关键行为差异)分两段**:起点"真人正活跃就搁置"恒
生效;执行中 cancel 由 `arise_cross_platform_pull_preemptible`(默认真)控制
——给"宁可不浪费已付 LLM 成本"的部署方一个逃生舱,但管不到起点那道闸。竞速用
`asyncio.wait` 而非在拉取内部埋检查点,就是为了不给被复用的既有执行核插桩。

**per-user 显式同意**(比习得贴纸的 per-chat 开关更进一步):本片连命令一起
做(`/允许了解我的动态`、`/不再了解我的动态`),刻意不加权限校验——同意主体是
内容所有者本人,与 chat 管理权限是两个不同授权维度。不做命令这功能就跟 #70 的
per-chat 开关一样落地即死代码。撤销不清 `last_pulled_at`,免得"撤销再同意"变成
绕过间隔节流的手段。

产出写既有画像治理(`sensitive` 硬编码为真——跨平台动态整类同质,不存在"这条
不敏感",无需一个可能被判错的字段)+ 落库前过人设漂移守护否决闸。

三处守卫已变异复核:同意闸挂 5 条、起点闸挂 2 条、可抢占竞速关掉后测试挂死
(据此给那条测试加了 `wait_for` 超时,挂死比断言失败难诊断)。
两轴各抓到实质缺陷,四条必修:

**1. 真 bug:工具一被调用整轮就炸(Standards 轴)**。注入的回调是同步 `def`,
handler 里却 `await` 它的返回值(`None`)→ TypeError,拉取永远不会发生。根因是
`_handle_pull_cross_platform` 这条路**零测试覆盖**——我测了候选/治理/命令,唯独
没测"模型真的调这个工具"。改回调为 async,并补三条覆盖该路径的测试(含工具可
见/不可见两向)。

**2. 同意键错配(Spec 轴)**。写入用 `event.get_user_id()`(原始适配器 id),
读取用 `chat_id.removesuffix(".p")`(host 经 ingress port 映射后)。两者不保证
相同——在做 id 归一化的 host 上,同意会写进一个永远查不到的键(静默失效),
或更糟,命中另一个人的 id。这是隐私机制,键错配后果比"功能不生效"严重。收敛到
纯函数 `consent_subject_of`,两侧都走 ingress port + 它。群聊无唯一主体,返回
`None` 并提示私聊说。

**3. 隐私外送(Standards 轴)**。`_known_platform_facts` 把该用户**全部**画像
事实(含住址/健康/财务这类 `sensitive=True`)拼进任务描述,交给 host 注册的抓取
工具(可能对外发请求)——与本机制"隐私分量最重、边界最紧"的立意直接相反,也与
`pull_task_description` 自己的文档不符。改为 `render_platform_facts` 只带非敏感
事实,并补测试。代价是可能漏查一个被判敏感的平台账号,这个方向是刻意选的。

**4. 同一轮多次调用起并发子智能体(Spec 轴)**。候选每轮只算一次,但模型在同一
轮工具循环里可以调多次,此前每次都起一个子智能体各付一份成本。加 per-user
in-flight 闸(`mark_pull_attempted` 在任务内部,拦不住同胞任务,所以闸必须在派发
这层)。

另外:竞速改 try/finally 收口 —— 原先只在抢占分支 cancel,漏了"外层任务被
cancel"这条路(`asyncio.wait` 被取消时不会连带取消它等的子任务,两个都悬空);
同意命令加 `to_me()` —— `COMMAND_START` 含空串时(本仓 .env.test 就是)群里有人
引用那句话就会被当成授权;删死代码 `PULL_INTERVAL_SECONDS`;活跃度窗口与轮询
间隔进 config(原先硬编码,测试只能按字面量 monkeypatch,改常量就静默退化成真
睡);`human_is_active` 提成纯函数移出 wiring 层。

新增守卫均已变异复核:in-flight 闸挂 1 条、敏感过滤挂 3 条。
Yushu merged commit 0a6a67a838 into main 2026-07-27 04:37:42 +00:00
Yushu deleted branch feature/71-cross-platform-pull 2026-07-27 04:37:42 +00:00
Sign in to join this conversation.
No description provided.