Files
PolyGateway/research-wiki/plans/2026-09-11-streaming-stall.md
T
iomgaa 929d60e501 docs: add 1.3.8 implementation plan and B4 design errata
B4: assembly-time non-streaming hedge warning operationalized to
embedding/OCR scopes only (chat stream is per-call, no assembly
signal); AFTER_S same-gap backfill included. Plan T1-T5 with real
grep sync lists and fail-first test batches.
2026-09-13 23:46:34 -04:00

52 KiB
Raw Blame History

type, node_id, title, date
type node_id title date
plan plan:2026-09-11-streaming-stall 1.3.8 流式中段沉默:计量口径与对冲实施计划 2026-09-11

1.3.8 流式中段沉默:计量口径与对冲实施计划

设计:research-wiki/designs/2026-09-11-streaming-stall-design.md,人类已批准 + 独立审查补遗 B1-B3/N1-N9 全处置并入,计划期补遗 B4(PM 2026-09-11 裁决)已并入 §6/§11;本计划一切语义以该设计为准,不另行发明。 计划审核门:Claude 自审 + 独立模型审查;plan 无人类门,审毕直接执行。 目标:① P1(issue #27)generation_ms 口径重定义为净生成时间(墙钟 − Σ 超阈值中段沉默),CallStats 增 stall_ms/stall_count 归因字段,遥测表加两列;② P2(issue #28){SCOPE}__HEDGE__STALL_AFTER_S 中段沉默对冲(独立生效,默认关闭);③ 缺省行为(两键均不设)逐字等于 1.3.7。 方案:P1 = 设计 §4.2 方案 1B'(净口径直接写进 generation_ms);P2 = 设计 §5.2 方案 2B(增量 chunk Event 等待超时触发,复用 1.3.7 对冲编排/记账/遥测全部语义)。 技术:Python 3.12+、asyncio 任务组编排(1.3.7 既有 _attempt_hedged 骨架扩两阶段触发窗)、frozen dataclass、pytest + 事件驱动假 transport + FakeClock/FakeSleep(净口径断言)与真实 loop 钟(对冲触发,4–10× 余量)、ruff、import-linter。 基线: main a28d944(1.3.7 已发布)+ 设计提交 97de410;分支 feature/1.3.8-streaming-stall。

范围纪律:不改四分类/熔断语义/429 分账/限流 Lua/deadline.py/Permit 端口/看门狗 streaming.py 语义;不做流式透传、内容拼接、自适应阈值、per-call 沉默阈值覆盖、embedding/OCR 对冲(设计 §8 非目标 A–F);不加墙钟/净双口径并存字段(active_generation_ms 否决);generation_ms 不进遥测列(YAGNI,墙钟 = generation_ms + stall_ms 还原);除设计 §7 已批准的三次端口变更外不动任何端口签名。

1. 边界、授权与执行纪律

项目 固定边界
唯一 writer 一工作区一 writer;父会话负责前台委派与审核派发。1.3.8 合并/发布授权沿用 1.3.X 惯例;跨到 1.4 或设计 §9 之外的新公共面须停下确认
公共面 只做设计 §7/§9 已批准项:Transport.complete 加 content_event 必填 kw(第二次端口变更)、TransportResult 增 stall_ms/stall_count、TelemetryRecorder.record_llm_call 加两参(第三次端口变更)、CallStats 增 stall_ms/stall_count、两配置键 {SCOPE}__STREAM__STALL_THRESHOLD_MS(默认 1000,可调)/{SCOPE}__HEDGE__STALL_AFTER_S(未设=关闭)、GatewayClient.__init__ 对应 keyword-only 参数、遥测表 stall_ms/stall_count 两列(可空)。generation_ms 语义重定义为净口径(已批准例外,披露义务见 T5)。不新增其它键/端口方法/遥测列/异常类
三次端口变更 ①Transport.complete 加 content_event(必填、无默认,与 first_token_event 同款约定);②TransportResult 增两字段(带默认 None);③record_llm_call 加 stall_ms/stall_count(无默认,装配闸判据即本签名)。全部同步点见 §2.2/§2.3 同步清单(真实 grep 结果,非凭记忆)
口径钉死(N8) TransportResult stall_ms: float | None;retry 层 wall_ms=int((now−gen_started)*1000) 照旧,net = max(0, wall_ms − int(stall_ms or 0))(clamp 防御);CallStats.stall_ms = int(stall_ms)(None 透传)——取整换算全库只此一处(retry 层),各实现不得另取整
钟同源纪律(N4) 沉默序列(transport 内 time.monotonic)与墙钟(retry 注入 now,生产即 time.monotonic)生产同钟;FakeClock 测试用自控 stall 值的假 transport,真实 transport 测试不注入假钟;对冲触发只用事件循环相对时长(asyncio.wait timeout),绝不读注入 now(1.3.7 §4.1 同款)
取消铁律 外部取消到达时对冲两路同消并穿透;不 shield、不留后台任务;_past_hedge_window 两阶段重构逐字保留 waiter 收口 + cancelling() 穿透纪律(retry.py:556-568 既有范式);stall 等待每轮迭代自建 waiter
Event 并发纪律(N3) ①超时到期后触发对冲前必须复检 is_set();②check→clear 之间不得有 await;③对冲路 content_event 恒 None(1.3.7 首 token 约定延伸,v1 不梯次);④等待者唯一(编排),Event 无需多等待者语义
排除条款 非流式 / 增量 chunk <2 → stall_ms/stall_count 为 None 且 generation_ms 不退减(= 原墙钟);chunk = 任何增量产出(content 或 reasoning_content 均计),空 chunk/usage 帧/SSE keepalive 不计时;对冲输家的沉默不进赢家快照(已完成落选者的沉默在其 attempt 遥测行;被取消输家无 TransportResult、hedge_cancelled 行 stall 恒 None)
降级方向 准入失败 → 静默放弃对冲(不抛错不硬等);限流/熔断后端不可用照常冒泡(fail-closed);遥测写失败 warning 降级;遥测两列对既有实例幂等 ALTER(auto_migrate 先探测后补列,manual 档裁剪+点名告警)
证据纪律 不打印 .env/token/Authorization;不提交 .pi/、tests/outputs/;测试事件驱动,禁 sleep 撞窗口;红绿证据落 tests/outputs/138/t<N>/(pytest 输出文件,不提交)

Skill 纪律:T1–T4 行为变更执行 test-driven-development(先红后绿证据落本会话工具输出 + tests/outputs/138/t<N>/ 文件);每次提交执行 commit;T5 前执行 requesting-code-review 与 verification-before-completion(全新上下文 verifier);异常先 systematic-debugging。

保真校验:本计划不涉及 reference/ 迁移(沉默计量与中段对冲为库内自研口径,蓝本即本库 1.3.7 编排与 streaming.py 看门狗),保真校验不适用。

2. 文件职责、同步清单与不变接缝

2.1 文件职责

动作 精确路径 职责
修改 src/polygateway/ports.py Transport.complete(:56-66)加 content_event 必填 kw + docstring;TelemetryRecorder.record_llm_call(:306-344)加 stall_ms/stall_count 两参 + docstring("36 字段"→"38 字段");TransportResult 在 types.py(见下)
修改 src/polygateway/types.py TransportResult(:560-586)增 stall_ms: float | None = None/stall_count: float | None = None(追加在 applied_effort 后);CallStats(:296-324)在 hedge_won 后增 stall_ms: int | None = None/stall_count: int | None = None;_CallContext(:328-388)__slots__/__init__ 增两私有位、record_generation 增 keyword-only stall_ms/stall_count(默认 None)、snapshot 填两字段;SourceConfig(:590-700)在 effort_fallback 后追加 stall_threshold_ms: int = 1000 + __post_init__ 值域校验(正整数、拒 bool)
修改 src/polygateway/transports/openai_compat.py complete(:427-473)加 content_event 透传两路径;_complete_stream(:553-616)chunk 循环(:580-590 邻域)记增量 chunk 时间戳 + 每 chunk set content_event(None 跳过),装配处(:597-616 邻域)由序列算 stall 两值;模块级纯函数 _stall_from_chunk_times;_complete_once(:660-705)接收 content_event 但永不置位(docstring 明写),stall 两值恒 None(默认)
修改 src/polygateway/middleware/retry.py __call__ 入口门(:273-283)改两阈值判定;_attempt(:304-400)加 content_event 私有 kw、sink 元素改三元组、成功分支取整换算 + _emit 传 stall;_attempt_hedged(:371-530)Phase 1/4 接线 content_event、Phase 5 汇合复用;_past_hedge_window(:533-562)两阶段重构;_record_generation/_register_hedge 邻域(净口径计算收敛一处);__init__ 加 stall_after_s kw
修改 src/polygateway/config.py _RESERVED_SEGMENTS(:57)加 "STREAM";GatewaySettings(:192-196 后)加 stall_threshold_ms: int = 1000、hedge_stall_after_s: float | None = None;_validate_hedge(:382-392)扩传新参;from_env(:398-427)**_load_stream(...);新增 _load_stream(:745 _load_hedge 邻域);check_hedge_assembly(:772-849)加 stall_after_s 参与三守卫;EmbeddingSettings.__post_init__(:865-869 邻域)与 OcrSettings(新增 __post_init__)加 B4 对冲键 warning
修改 src/polygateway/client.py __init__(:240 邻域)加 hedge_stall_after_s/stall_threshold_ms keyword-only 参数,守卫(:252-257)扩传;stall_threshold_ms 经 dataclasses.replace 分发进各 SourceConfig;RetryMW 构造(:264-283)传 stall_after_s;from_settings(:485-526)透传
修改 src/polygateway/middleware/telemetry.py _AttemptUsage(:199-236)增两字段;emit_attempt(:396-447)加 stall_ms/stall_count kw(默认 None)+ 转发;emit_cache_hit/emit_terminal_failure(:449-540)显式传 None;_record(:560-665)加两参并在 :618 调用点透传
修改 src/polygateway/telemetry/schema.py 五编辑点:SQLITE_DDL/PG_DDL 末尾(total_latency_ms 后)加两列;SQLITE_BACKFILL(:113-141)追加两项;_PG_BACKFILL_DECLS(:144-166)追加两项(保序对齐);COLUMNS(:177-215)追加两名;telemetry_schema_sql(:323-372)由常量派生同源再生成(本函数零编辑,列数断言随动);模块头 "36/37" 计数 docstring 同步
修改 src/polygateway/telemetry/sqlite.py / postgres.py record_llm_call(:145/:411)为 **fields 形态,零签名改动自动拾取新列;docstring "36 字段冻结签名"→"38 字段";补列/裁剪逻辑由 schema 常量驱动,零代码改动
修改 tests/unit/test_openai_compat.py 批次 A(content_event 四用例 + stall 纯函数用例);_complete helper(:82-93)同步签名
新建 tests/unit/test_stall_hedge.py 批次 D 中段对冲编排用例(与 1.3.7 test_hedge.py 同设施范式,不动该文件既有断言)
修改 tests/unit/test_types.py test_retry.py test_config.py test_client.py test_telemetry.py test_ports.py 批次 B/C/E 与 fake 同步(§2.2/§2.3);test_telemetry.py 冻结列清单按 T3 扩展(:53-99、:265、:295-297、:374 等断言点)
修改 tests/unit/test_backpressure.py:213 test_ports.py:75 test_client.py:224/1827 test_retry.py:82/112 test_hedge.py:55 tests/integration/test_redis_cross_connection.py:78 tests/e2e/conftest.py:285 tests/unit/test_live_evidence.py:280/1270 tests/unit/test_usage_source_domain.py:142 T1 端口签名同步(e2e 包装转发 content_event;详见 §2.2 全量清单)
修改 tests/integration/test_postgres_telemetry.py _EXPECTED_COLUMNS(:33-77 邻域)追加两列;_CALL_OBSERVABILITY_COLUMNS(:84)为派生式自动纳入,manual 档告警比对用例随动核对
修改 CHANGELOG.md、README.md、.env.example、research-wiki/ARCHITECTURE.md、research-wiki/ROADMAP.md T5 文档与登记(§4 T5)
新建 research-wiki/findings/2026-09-11-streaming-stall-validation.md 红绿、命令、豁免索引,≤300 行

不改:errors.py/deadline.py/streaming.py/middleware/{ratelimit,breaker,structured,cache,admission}.py/backends/**/embedding.py/ocr.py 链路本体(仅其 emit_attempt 调用点经默认 None 自然兼容,零改动)/tests/contracts/**;StallClock/backoff_delay/settle_and_release/_combine_failures 逐字不动。若必须突破本清单,先说明最小原因交父会话核定。

2.2 T1 同步清单:Transport.complete 加 content_event(真实 grep 结果)

执行于 2026-09-11,命令:grep -rn "def complete\|first_token_event" tests/ src/ | grep -v __pycache__(剔除 tests/outputs/ 历史日志后)。content_event 与 first_token_event 同签名形态(必填 kw、asyncio.Event | None),下列每一处都要同步。

实现/协议(11 处 def complete):

# 位置 处置
1 src/polygateway/ports.py:56(Transport 协议) 加 content_event 必填 kw + docstring(None = 不观测增量 chunk;非流式永不置位;对冲路恒 None)
2 src/polygateway/transports/openai_compat.py:427(真实实现) 加参透传 _complete_stream(:469)/_complete_once(:473)两路径
3 src/polygateway/transports/openai_compat.py:560(_complete_stream) 加参;每增量 chunk 置位(:587-588 首 token 块邻域扩展为每 chunk)
4 src/polygateway/transports/openai_compat.py:666(_complete_once) 接收但永不置位,docstring 明写
5 tests/unit/test_retry.py:82(FakeTransport) 签名加 content_event,不给默认值(test_retry.py:71-73 既有注释同款纪律)
6 tests/unit/test_retry.py:112(第二个 fake) 同上;:123 调用处转发
7 tests/unit/test_hedge.py:55(事件驱动假 transport) 加参并逐次记录身份(:50/:59 同款,供"对冲路恒 None"断言)
8 tests/unit/test_backpressure.py:213 加参忽略
9 tests/unit/test_ports.py:75 加参忽略
10 tests/unit/test_client.py:224 与 :1827 加参忽略
11 tests/integration/test_redis_cross_connection.py:78 / tests/e2e/conftest.py:285 e2e/conftest 包装必须转发 content_event(:294/:306 同款);integration fake 加参忽略

调用点(11 处):

# 位置 处置
1 src/polygateway/middleware/retry.py:275(单路路径) T1 先传字面 content_event=None;T4 接线真事件
2 src/polygateway/middleware/retry.py:344(_attempt 内) 透传 _attempt 的 content_event kw
3 src/polygateway/middleware/retry.py:436(对冲原路) T4 接线:流式且 stall_after_s 已设才给真事件,否则 None
4 src/polygateway/middleware/retry.py:484(对冲路) 恒 content_event=None(N3③,v1 不梯次)
5 src/polygateway/transports/openai_compat.py:469/:473 两路径透传(见实现 #2)
6 tests/unit/test_openai_compat.py:84/:93(_complete helper) helper 加 content_event=None 默认转发(测试设施,与生产端口"必填无默认"约定不冲突)
7 tests/unit/test_live_evidence.py:280 与 :1270 两直调传 content_event=None;不传则必填 kw 报 TypeError,unit 门必红
8 tests/unit/test_usage_source_domain.py:142 直调传 content_event=None

2.3 T3 同步清单:TelemetryRecorder.record_llm_call 加两参(真实 grep 结果)

执行于 2026-09-11,命令:grep -rn "record_llm_call" tests/ src/ --include="*.py" | grep -v __pycache__(剔除 tests/outputs/ 历史日志)。

# 位置 处置
1 src/polygateway/ports.py:306(协议) 末尾追加 stall_ms: int | None、stall_count: int | None(无默认值,与既有 36 参同款约定);docstring "36 字段"→"38 字段"。本签名是装配闸唯一事实源,漏改即全部 **fields recorder 在装配期 signature.bind 炸——这是有意的(1.3.5 先例)
2 src/polygateway/middleware/telemetry.py:618(唯一调用点 _record) 显式传 stall_ms=/stall_count=;_record 签名加两参
3 src/polygateway/telemetry/sqlite.py:145 **fields 形态,零签名改动;行取值按探测列裁剪,新列由 schema 常量驱动自动纳入;docstring 计数同步
4 src/polygateway/telemetry/postgres.py:411 同上
5 tests/unit/test_ports.py:102(_DummyRecorder,显式全签名) +2 参(它镜像协议全签名,不加则与本协议脱节)
6 tests/unit/test_telemetry.py:3327/:3349/:3354(装配闸负例 fake,故意残缺签名) 不改——协议加参后它们仍不满足签名,负例断言照常通过;T3 绿态核对这三条仍绿
7 **fields fake(13 处):test_ocr_client.py:149、test_pricing.py:131、test_openai_compat.py:106、test_embedding.py:246、test_cache.py:562、test_client.py:528/955/968/1516/1842、test_usage_source_domain.py:71、test_telemetry.py:1191/2059 零改动自动兼容;批次 C 断言落行时用其中一份捕获 fields
8 发射器 fake:tests/unit/test_hedge.py:122、tests/unit/test_backpressure.py:233(emit_attempt) 零改动(emit_attempt 新参默认 None,见 §3.4 取舍)

3. 跨任务接口(可执行定义,禁止占位)

3.1 T1:观测原语与端口契约(P1 底层 + P2 信号源)

端口签名(ports.py:56-66,顺序追加在 first_token_event 后,必填、不设默认值,同款约定):

async def complete(
    self, *, messages, source, stream, overlay, call_id, reasoning_effort,
    first_token_event: asyncio.Event | None,
    content_event: asyncio.Event | None,
) -> TransportResult: ...

docstring 补:None = 调用方不观测增量 chunk(未启用中段对冲);流式实现每到达一个增量 chunk(content 或 reasoning_content,与看门狗/首 token 活性口径一致)置位一次;空 chunk/usage 帧/SSE keepalive 不算;非流式实现永不置位;对冲路恒收 None(v1 不梯次,N3③)。

TransportResult(types.py:560-586,追加在 applied_effort 后):

stall_ms: float | None = None
"""超阈值相邻增量 chunk 沉默总额(毫秒,未取整;换算唯一处在 retry 层,设计 §4.2 N8)。"""
stall_count: float | None = None
"""超阈值相邻间隔个数;非流式/增量 chunk <2 → None(排除条款照 vLLM,设计 §4.2)。"""

SourceConfig.stall_threshold_ms(types.py effort_fallback 后追加,int = 1000):这是钉死的传入路径裁决。任务给的两条候选中:走 complete 新 kw = 设计 §7 未批准的第四次端口变更(且 §2.2 清单 22 处全部二次翻动,计量口径被耦进端口契约),否决;走 SourceConfig 字段与 inter_token_timeout_s(types.py:612)同先例——transport 在 _complete_stream 直接读 source.stall_threshold_ms,端口零新增,watchdog 成对校验的 _validate_watchdog(:676-681)同款落点加值域校验(正整数、拒 bool)。scope 级键 → per-source 字段的分发由装配层 dataclasses.replace 完成(T4,§3.5),运行时零额外接线。符合"决策逻辑与状态存储分离":字段是纯配置不可变数据,计算逻辑在 transport,编排判定在 retry。

_complete_stream 原语(:580-590 max_gap 循环邻域):循环内已有逐 chunk 迭代,追加两件零判定动作——chunk_times.append(now)(或等价的增量累计,见下)与 content_event.set()(None 跳过);热路径不做阈值判定(设计 §3 派生原则)。stall 计算收敛为模块级纯函数(P6):

def _stall_from_chunk_times(times: list[float], threshold_ms: int) -> tuple[float | None, float | None]:
    """增量 chunk 时间戳序列 → (stall_ms, stall_count);chunk <2 → (None, None)。

    沉默 = 相邻间隔**严格大于**阈值(恰好 = 阈值不减,pi-tps 口径);
    尾部(末 chunk → 流结束)不计——墙钟基底口径,尾部归活跃侧(设计 §4.2 N1)。
    """
    if len(times) < 2:
        return None, None
    total = 0.0
    count = 0
    for prev, cur in zip(times, times[1:]):
        gap_ms = (cur - prev) * 1000
        if gap_ms > threshold_ms:
            total += gap_ms
            count += 1
    return (total, float(count)) if count else (0.0, 0.0)

注意:无超阈段但 chunk ≥2 时返回 (0.0, 0.0) 而非 None——"观测过且零沉默"与"无从观测"(None)是两回事,下游预算公式以 stall_ms is None 为分支条件(设计 §4.2 N2)。_complete_stream 装配处(:597-616 邻域)填 stall_ms=/stall_count=;_complete_once 走默认值 None。max_inter_token_ms 原语义一字不动。

3.2 T2:净口径记账(P1 落账)

CallStats(types.py:296-324,hedge_won 后追加):

stall_ms: int | None = None
"""赢家路超阈值中段沉默总额(净口径归因;None = 非流式/chunk<2/无赢家)。墙钟 = generation_ms + stall_ms。"""
stall_count: int | None = None
"""超阈值相邻间隔个数(归因成对报告,Gregg 原则,调研 §B2)。"""

_CallContext(:328-388):__slots__/__init__ 加 _stall_ms/_stall_count(初值 None);record_generation 增 keyword-only stall_ms: int | None = None, stall_count: int | None = None(chat 赢家路传入;embedding/ocr 既有调用零改动,恒 None 即"无沉默观测");snapshot 填两字段。

retry 层换算唯一处(N8):_attempt 成功分支在现有 sink append 处(retry.py:344-347 邻域)把 sink 元素从 int 升级为三元组:

wall_ms = int((self._now() - gen_started) * 1000)
stall_ms_i = None if result.stall_ms is None else int(result.stall_ms)      # 取整只此一处
stall_count_i = None if result.stall_count is None else int(result.stall_count)
generation_sink.append((wall_ms, stall_ms_i, stall_count_i))

净口径计算收敛进共享守卫 _record_generation(单路)与 _register_hedge(对冲赢家)共同调用的一个内部换算:net = max(0, wall_ms - (stall_ms_i or 0))(clamp 防御,N4),然后 context.record_generation(net, accumulate=False, stall_ms=stall_ms_i, stall_count=stall_count_i)。三条消费路径:

路径 位置 口径
单路成功 __call__ :283-288 邻域(generation_sink 非空分支) net + stall 两值进快照
对冲赢家 _register_hedge(winner_sink 非空) 赢家路 net + 赢家路 stall 两值;输家沉默不进赢家快照(设计 §4.2 排除条款)
两败(无赢家) _register_hedge(hedge_won=False, winner_sink=None) 不记 generation、stall 保持 None(无归属);两路各自的沉默在各自 attempt 遥测行

非流式/chunk<2:TransportResult stall 恒 None → net = wall(不退减),快照 stall 两值 None(设计 §4.2 排除条款原样落地,无需分支——None 透传天然达成)。

3.3 T2:FakeClock 纪律下的测试设施

净口径断言用 test_retry.py:120-127 既有 FakeSleep/FakeClock 范式 + 自控 stall 假 transport:fake 委托现有 FakeTransport,返回前 clock.advance(wall) 并在 TransportResult 上填固定 stall_ms/stall_count(假 transport 自己报数,不经过真实计时)——真实 transport(openai_compat 内部 time.monotonic)不注入假钟(N4)。精确值断言只允许在这类注入钟 + 自控 stall 的用例里;真实 loop 钟用例只断下界与相对比较(1.3.7 §6 同款纪律)。

3.4 T3:遥测两列(P1 归因进库)

schema.py 五编辑点(顺序与 1.3.5 十列同款纪律;新列全部可空、无默认值、追加在末尾——旧行 NULL 表达"补列之前根本没记过这件事",不回填):

编辑点 内容
① SQLITE_DDL/PG_DDL 末尾 total_latency_ms 后加 stall_ms INTEGER / stall_count INTEGER(PG 同名 INTEGER;不用 REAL——落库值是 retry 层已取整的 int,口径与 N8 一致)
② SQLITE_BACKFILL(:113-141) 追加 ("stall_ms", "INTEGER")、("stall_count", "INTEGER")
③ _PG_BACKFILL_DECLS(:144-166) 追加同名两项,与 ② 逐项保序对齐(两路径物理列序不许分叉)
④ COLUMNS(:177-215) 末尾追加 "stall_ms", "stall_count"
⑤ telemetry_schema_sql(:323-372) 零编辑——由 ②③ 常量派生,同源再生成天然达成;列数断言测试随动(38)

协议与发射器:ports.py:306 协议 +2 参(§2.3 #1);_record +2 参并在 :618 透传;_AttemptUsage +2 字段(默认 None;of() 不从 response 取——LLMResponse 不增顶层字段,设计 §11-B2 裁决 (b));emit_attempt 加 keyword-only stall_ms: int | None = None, stall_count: int | None = None 显式转发。默认值取舍:与 reasoning_applies 不同——stall 漏传落 None 是合法事实("非流式/无沉默观测"),不是"没表态",默认 None 语义安全且保住约 50 处既有 emit_attempt 关键字调用点零改动(反 gold-plating);chat 成功路径漏传会被批次 C 落行断言当场抓住。emit_cache_hit/emit_terminal_failure 显式传 None(缓存命中与终态行无 transport 观测)。

retry 侧 _emit(:665-700):加 stall_ms/stall_count kw(默认 None);_attempt 成功分支把 §3.2 已算好的 stall_ms_i/stall_count_i 传入;失败/取消分支不传(恒 None——被取消输家无 TransportResult,hedge_cancelled 行 stall 恒 None,其存在性由该行表达,设计 §11-B2)。embedding.py:532/ocr.py:611 调用点零改动(默认 None 即正确语义)。

3.5 T4:配置两键 + 守卫 + 装配分发

STREAM__STALL_THRESHOLD_MS(计量阈值,设计 §6 第一行):

项 精确定义
保留段 :57 _RESERVED_SEGMENTS 加 "STREAM"(防 4 段 {SCOPE}__STREAM__{N}__* 撞 provider 段,1.3.7 HEDGE 同款);3 段键天然跳过 _load_sources(:427-431 段数判据)
loader 新增 _load_stream(scope, env)(:745 _load_hedge 邻域):_first + _cast(..., "int", ...),origin 传实际命中键名;值域(正整数)归 SourceConfig.__post_init__ 一处(字段在 §3.1 已钉死);返回 {"stall_threshold_ms": ...},未设 = 1000
字段 GatewaySettings :196 后加 stall_threshold_ms: int = 1000
分发 装配层把 scope 值经 dataclasses.replace(source, stall_threshold_ms=...) 写进每个 SourceConfig:from_settings(client.py:485-526)恒分发(settings 值权威);GatewayClient.__init__ 加 keyword-only `stall_threshold_ms: int

HEDGE__STALL_AFTER_S(对冲触发,设计 §6 第二行 + §11-N5/B4):

项 精确定义
loader _load_hedge(:745-769)扩读 STALL_AFTER_S:_first + _cast + ensure_call_deadline(origin 传实际命中键名),返回字典加 "hedge_stall_after_s" 键;未设 = None(关闭)
字段 GatewaySettings 加 hedge_stall_after_s: float | None = None;_validate_hedge(:382-392)扩传
守卫(全部落进 check_hedge_assembly :772-849,签名加 stall_after_s 参,None 时值域归一化后交叉守卫不查——同 after is None 既有早退形态) ①值域 = ensure_call_deadline 同款(有限正数);②stall_after_s ≥ min over sources of (inter_token_timeout_s or timeout_s)(有效最小值,与 openai_compat.py:573 回落同款)→ ValueError(对冲窗永不开,装配期直接拒绝;stall 场景无非流式残留收益,warning 论证不足,N5);③stall_after_s ≥ call_deadline_s(两者皆设)→ ValueError(与 after_s 同款,config.py:825-828 邻域);④单源 scope 设阈值 → warning 允许(1.3.7 §5 同款);⑤B4:Embedding/OCR scope 装配 warning 落 EmbeddingSettings.__post_init__(:865-869 邻域)与 OcrSettings(新增 __post_init__):内嵌 gateway 的 hedge_after_s 或 hedge_stall_after_s 非 None → warning"对冲键仅对 chat scope 生效,本 scope 非流式静默退化"(1.3.7 AFTER_S 同病顺带补齐,缺陷修复不扩公共面);chat 侧装配 warning 明确不做(per-call stream,装配期无信号,B4 裁决)
client GatewayClient.__init__ 加 hedge_stall_after_s: float | None = None keyword-only,:252-257 守卫调用扩传;RetryMW 构造(:264-283)传 stall_after_s= 归一化值;from_settings(:525 邻域)透传

3.6 T4:retry 编排改动(P2 唯一实现形态)

RetryMW.__init__ 加 keyword-only stall_after_s: float | None = None(存 self._stall_after_s;守卫在装配层,本类不重复校验——同 hedge_after_s 先例)。

__call__ 入口门(:273-283 重构,设计 §11-B3):

if self._hedge_after_s is None and self._stall_after_s is None:
    # 两阈值均未设: 逐字 1.3.7 单路路径(默认关闭回归门据此成立)
    outcome = await self._attempt(..., first_token_event=None, content_event=None, ...)
elif self._hedge_after_s is None and not request.stream:
    # 仅 stall 已设且请求非流式: 中段沉默无非流式概念,逐字走单路(等价未配置)
    outcome = await self._attempt(..., first_token_event=None, content_event=None, ...)
else:
    outcome = await self._attempt_hedged(request, picked, reasons, attempt_fails)

_attempt_hedged 接线:Phase 1 原路事件的构造改为——first_token 恒建(AFTER_S 已设时要等它;仅 stall 时阶段 2 入口也要先等它,见下);content_event = asyncio.Event() if (request.stream and self._stall_after_s is not None) else None,随 content_event= 传入 _attempt。Phase 4 对冲路恒 first_token_event=None, content_event=None(N3③)。Phase 5 赢家裁定/收口/"先了结的是失败则等另一路"/_combine_failures/两败一次预算逐字复用——主路被看门狗掐死(Transient)时落入既有"先了结的是失败"分支等副路,attempt 级记账(breaker record_failure/attempt_fails/遥测 error 行)照常(设计 §11-N9;不对称声明:掐死喂熔断 vs 输家取消不喂,本版不改任一)。

_past_hedge_window 两阶段重构(:533-562;waiter 收口 + cancelling() 穿透纪律逐字保留,签名加 content_event):

async def _past_hedge_window(self, primary, first_token, content_event) -> bool:
    # 阶段 1 首 token 前: AFTER_S 已设才等(设计 §5.2;仅 stall 时此阶段不触发,
    # 但仍须等首 token 到达才能进阶段 2——否则 pre-TTFT 沉默会被 stall 窗误吞)
    if self._hedge_after_s is not None:
        <既有 waiter 形态: asyncio.wait({primary, waiter(first_token)},
             timeout=self._hedge_after_s, FIRST_COMPLETED),finally 收口 + cancelling 穿透>
        if not primary.done() and not first_token.is_set():
            return True                     # 首 token 超时未至 → AFTER_S 触发
    else:
        <同形态 waiter 但无 timeout: 只等 first_token 或 primary 了结>
    if primary.done():
        return False
    # 阶段 2 首 token 后: STALL_AFTER_S 循环等待;非流式/未设 stall → 不开窗
    if self._stall_after_s is None or content_event is None:
        return False
    while not primary.done():
        waiter = asyncio.create_task(content_event.wait())   # 每轮自建 waiter(N3④)
        try:
            await asyncio.wait({primary, waiter}, timeout=self._stall_after_s,
                               return_when=asyncio.FIRST_COMPLETED)
        finally:
            <waiter.cancel() + await 收口 + cancelling() 穿透,逐字套用既有范式>
        if primary.done():
            return False
        if content_event.is_set():        # N3① 复检: chunk 与超时同 pass 到达 → 防误触发
            content_event.clear()         # N3② check→clear 之间无 await
            continue
        return True                        # 复检仍未置位 = 真沉默 → 触发对冲
    return False

红线:阶段 2 的 clear() 只许出现在 is_set() 为真且两者间无 await 的紧邻位置;每轮 waiter 必须进 finally 收口;外部取消在任一阶段穿透两路(_attempt_hedged 既有 except BaseException 收口块不动)。

4. 任务与提交点(T1→T2→T3→T4→T5;T3 可并 T2 后)

依赖顺序:T1(端口+原语)→ T2(记账,消费 T1 的 TransportResult 字段)→ T3(遥测,消费 T1/T2 的口径;可在 T2 后并行,因只碰 telemetry/ports/retry._emit 邻域)→ T4(编排+配置,消费 T1 的 content_event 与 T2 的快照口径)→ T5(文档,需前四者行为冻结)。每任务卡:目标 / 改动文件:行号邻域 / 先失败测试清单 / 证据出口 / 验收命令 / 回滚点。所有命令 conda run -n PolyGateway,禁接管道(退出码失真教训);证据文件不提交。

T0:设计 B4 补遗与本计划(本任务,无代码)

产出:设计 §6 配置表 STALL_AFTER_S 行修正 + §11 补 B4 行(已完成,2026-09-11 PM 裁决)+ 本计划。不提交代码。

T1 → 提交 1 feat: add a content event and stall accounting to the transport port

  • 目标:观测原语(增量 chunk 时间戳 + content_event)+ 端口契约(complete 加 kw、TransportResult/SourceConfig 增字段)+ stall 纯函数;§2.2 同步清单全量落地。
  • 改动:ports.py:56-66;types.py:560-586/590-700;openai_compat.py:427-473/553-616/660-705;retry.py:275/314/344/436/484(T1 阶段恒传 None);§2.2 表 11 实现 + 11 调用点全量同步。
  • 先失败测试清单(批次 A;先红证据 = TypeError/ImportError/属性缺失,非断言值不符):
测试名 断言点
test_openai_compat.py::TestContentEvent::test_stream_sets_content_event_per_chunk 流式 N 个增量 chunk → event 被置位(每 chunk 一次;计数/逐次 is_set 观测)
…::test_reasoning_chunk_counts_as_liveness 只流 reasoning_content 的流也置位 content_event 且计入 chunk 序列(B1:思考增量同样是存活证据)
…::test_non_stream_never_sets_content_event 非流式永不置位
…::test_none_content_event_keeps_behavior None = 不观测,行为逐字不变
…::test_content_event_is_required_keyword 漏传 → TypeError(钉住必填)
…::TestStallFromChunkTimes::test_gap_above_threshold_accumulates 构造时间戳序列 [0, 0.3, 2.0, 2.1] 阈值 1000ms → stall_ms≈1700、stall_count==1(多段序列 → 总额/次数)
…::test_gap_exactly_at_threshold_not_deducted 间隔恰好 == 阈值 → 不减(严格大于)
…::test_fewer_than_two_chunks_returns_none 0/1 个 chunk → (None, None)(vLLM 排除条款)
…::test_tail_not_counted 末 chunk 之后的流结束间隔不进 stall(尾部归活跃侧,N1)
…::test_empty_or_usage_frames_not_timed 空 chunk/usage 帧/keepalive 不产生时间戳记录点
test_types.py::test_transport_result_stall_fields_default_none 两字段默认 None,旧位置构造不炸
test_types.py::test_source_config_stall_threshold_validation 默认 1000;0/负/bool → ValueError
  • 证据出口:tests/outputs/138/t1/red-batch-a.log(先红)+ green-unit.log(后绿)。
  • 验收命令:pytest tests/unit/test_openai_compat.py tests/unit/test_types.py -q → pytest tests/unit -q(全绿且不改一行既有断言,§2.2 同步遗漏会在此暴露为 TypeError)。
  • 回滚点:git revert <提交1>(同步清单全量在同一提交,revert 即全回)。

T2 → 提交 2 feat: redefine generation_ms as net generation time with stall attribution

  • 目标:generation_ms 净口径 + CallStats/_CallContext 两归因字段 + retry 三路径赢家记账(换算唯一处)。
  • 改动:types.py:296-324/328-388(§3.2);retry.py:283-288/332-360 邻域 + _record_generation/_register_hedge 共享换算(§3.2 表)。
  • 先失败测试清单(批次 B;先红 = 字段不存在 TypeError / 断言仍得墙钟值):
测试名 断言点
test_types.py::test_callstats_stall_fields_default_none 两字段默认 None;仅旧参数构造 CallStats(...) 不炸(1.3.7 及以前兼容)
test_types.py::test_callcontext_record_generation_with_stall_and_snapshot stall 两值经 record_generation 进 snapshot;不传恒 None
test_retry.py::test_generation_ms_net_of_stall FakeClock + 自控 stall 假 transport:wall 推进 5.0s、stall_ms=3200 → generation_ms == 1800 且 stall_ms == 3200、stall_count 透传;generation_ms + stall_ms == wall(还原公式)
test_retry.py::test_generation_ms_clamps_negative_to_zero stall > wall(假 transport 报 6.0s stall、wall 5.0s)→ generation_ms == 0(clamp,N4)
test_retry.py::test_non_stream_stall_none_no_deduction stall None → generation_ms == wall(不退减),快照两值 None(排除条款)
test_retry.py::test_stall_attribution_none_when_no_winner 两败轮次(复用 1.3.7 两败设施)→ snapshot stall 两值 None、generation_ms 不记
  • 证据出口:tests/outputs/138/t2/red-batch-b.log + green-unit.log。
  • 验收命令:pytest tests/unit/test_types.py tests/unit/test_retry.py -q → pytest tests/unit tests/contracts -q(全绿;对冲赢家口径断言在 T4 编排落地后补,见批次 D 末行)。
  • 回滚点:git revert <提交2>。

T3 → 提交 3 feat: add stall_ms and stall_count telemetry columns

  • 目标:遥测两列全链路(schema 五编辑点 + 协议两参 + emitter 透传 + 两 recorder 自动拾取);§2.3 同步清单落地。
  • 改动:telemetry/schema.py 五编辑点(§3.4 表)+ 模块头计数 docstring;ports.py:306-344;middleware/telemetry.py:199-236/396-447/449-540/560-665;retry.py:_emit(§3.4);sqlite.py:145/postgres.py:411 docstring 计数;test_ports.py:102 +2 参;test_telemetry.py 冻结清单扩展;tests/integration/test_postgres_telemetry.py:33-77 冻结清单扩展。
  • 先失败测试清单(批次 C;先红 = 冻结清单已扩展而 schema 未改 → 列数/列序断言失败;协议未加参 → 装配闸 ValueError):
测试名 断言点
test_telemetry.py::TestSchemaFreeze::test_columns_and_ddl_are_frozen(扩) _EXPECTED_COLUMNS 末尾追加两列后:len(COLUMNS) == 38;两端 DDL 首现顺序含新列;PG_BACKFILL[-1] 变 stall_count 项;占位符 $38)/?×38
test_telemetry.py::TestBackendColumnParity::test_new_columns_are_appended_last(扩) COLUMNS[-2:] == ("stall_ms", "stall_count")
test_telemetry.py::<SQLite 落行新用例> emit attempt(stall_ms=3200, stall_count=2)→ SQLite 行两列恰为该值;success 非流式行两列 NULL
test_telemetry.py::<迁移幂等新用例> 既有 1.3.5 形态表(37 物理列)开 auto_migrate recorder → ALTER 补两列不炸;重复初始化第二次幂等;manual 档缺列告警逐字含两条新 ALTER
test_hedge.py::<取消输家行新用例> 既有 AFTER_S 对冲触发,输家 hedge_cancelled 行 stall_ms/stall_count 恒 None(被取消输家无 TransportResult,B2)
test_telemetry.py 装配闸负例三条(:3327/:3349/:3354) 不改,协议加参后仍绿(负例仍旧不满足签名)
  • 证据出口:tests/outputs/138/t3/red-batch-c.log + green-unit.log;PG 实库形态由 tests/integration/test_postgres_telemetry.py 覆盖(slow,发布门交集见 §6)。
  • 验收命令:pytest tests/unit/test_telemetry.py tests/unit/test_ports.py tests/unit/test_hedge.py -q → pytest tests/unit tests/contracts -q。
  • 回滚点:git revert <提交3>(DDL 为可空加列,旧代码读新表兼容;回滚后新列闲置 NULL)。

T4 → 提交 4 feat: add opt-in mid-stream stall hedging for chat

  • 目标:HEDGE__STALL_AFTER_S 独立生效的中段对冲 + STREAM__STALL_THRESHOLD_MS 装配三层可调 + 全部守卫(B4 含 1.3.7 AFTER_S 同病补齐)。
  • 改动:config.py:57/192-196/382-392/398-427/745-769/772-849/865-869/OcrSettings 新 __post_init__(§3.5);client.py:240/252-257/264-283/485-526(§3.5);retry.py:__init__/:273-283/:371-530/:533-562(§3.6)。
  • 先失败测试清单(批次 D;先红 = 键未识 ValueError/None 断言失败、对冲未触发 hedges==0、挂起用例超时):
测试名(新建 tests/unit/test_stall_hedge.py,设施复用 test_hedge.py 范式:两源、event-driven 假 transport、真实 loop 钟、阈值 0.05s、容差 4–10×) 断言点
test_stall_triggers_hedge_after_first_token 假 transport 首 chunk 后沉默:s1 挂起、s2 即时成功 → 对冲触发,hedges==1、hedge_won is True;generation_ms 为赢家路净口径(T2 批次 B 顺延的对冲赢家断言落这里)
test_stall_recheck_prevents_false_trigger chunk 在超时到期的同一事件循环 pass 到达(0.049s 置位,阈值 0.05s)→ 不触发,attempts==1(N3① 复检)
test_stall_and_after_s_independent_matrix 参数化四格:仅 AFTER_S / 仅 STALL_AFTER_S / 两者 / 皆无 → 各格触发语义与设计 §5.2 逐字一致(仅 stall 时 pre-TTFT 沉默不触发——阶段 1 不开窗)
test_stall_only_non_stream_degrades_to_single_leg 仅 stall 已设 + stream=False → 逐字单路,attempts==1、hedges==0(入口门)
test_watchdog_kills_primary_hedge_leg_survives 主路首 chunk 后沉默超 inter_token_timeout_s 被看门狗掐死、副路已在跑 → 本轮不因主路之死而败,副路结果裁定;主路源 failure_count + 1(attempt 记账照常,N9)、遥测主路 error 行 + 副路正常行
test_both_fail_counts_budget_once 主路看门狗掐死 + 副路 Transient → fails 只计一次(_combine_failures 复用),两败轮 stall 快照 None
test_external_cancel_during_stall_wait_penetrates stall 等待中(阶段 2 循环内)外部 task.cancel() → CancelledError 穿透、waiter 收口无残留任务、两路(若已发)同消;1.3.7 test_external_cancel_cancels_both_legs 既有断言一字不改仍绿
test_hedge_leg_content_event_always_none 对冲路收到的 content_event 恒 None(N3③,test_hedge.py:50/:59 同款逐次身份记录)
test_config.py::test_stall_threshold_key_loads_three_layers env 键 / GatewaySettings 字段 / GatewayClient kwarg 三层均可调(§9-③ 人类要求);SourceConfig 分发经 replace 生效;未设恒 1000
test_config.py::test_stream_segment_reserved STREAM 在保留段,LLM__STREAM__1__X 不造源;3 段键跳过 _load_sources
test_config.py::test_stall_after_s_guard_effective_minimum_raises stall_after_s ≥ min(inter_token_timeout_s or timeout_s) → ValueError(有效值回落同款;配 watchdog 的源用 inter_token,未配的用 timeout_s)
test_config.py::test_stall_after_s_guard_deadline_conflict_raises ≥ call_deadline_s → ValueError
test_config.py::test_stall_after_s_single_source_warns 单源 warning 允许(loguru 捕获 fixture,test_config.py:31-33 既有)
test_config.py::test_embedding_ocr_scope_hedge_keys_warn Embedding/OCR settings 内嵌 gateway 的 AFTER_S 或 STALL_AFTER_S 非 None → warning(B4,含 1.3.7 同病补齐);chat 侧无此 warning(B4 裁决)
批次 E 默认关闭回归门 两键均不设 → pytest tests/unit tests/contracts -q 全绿且不改一行既有断言(1.3.6/1.3.7 取消/对冲/看门狗语义回归)
  • 证据出口:tests/outputs/138/t4/red-batch-d.log + green-unit.log + green-contracts.log + lint.log。
  • 验收命令:pytest tests/unit/test_stall_hedge.py tests/unit/test_config.py -q → pytest tests/unit tests/contracts -q → make lint。
  • 回滚点:git revert <提交4>(两键默认关闭,revert 后配置键失效由 _load_* 缺省自然达成)。

T5 → 提交 5 docs: document net generation time and mid-stream stall hedging

  • 目标:披露义务与登记(设计 §4.2/§7);无代码行为变更。
  • 改动清单:
    1. CHANGELOG.md 未发布段:设独立"行为变更"段显著标注——generation_ms 1.3.7(墙钟)→ 1.3.8(净生成时间 = 墙钟 − Σ 超阈值中段沉默,仍含 TTFT 与尾部),还原公式 墙钟 = generation_ms + stall_ms;正当性三行(字段发布仅 2 天/消费者唯一/遥测无账本,设计 §4.2);max_inter_token_ms 原语义不变;新两字段/两键/遥测两列;对冲强制措辞三句(默认关闭;开启即用配额换延迟——实测沉默 >20s 占 19.0%,触发率远高于首 token 对冲;输家可能已被上游计费);"中段沉默对冲无业界公开先例"诚实标注(调研 §A3);看门狗掐死喂熔断 vs 对冲输家取消不喂的不对称声明(N9)。
    2. README.md:口径表(generation_ms 净口径定义/边界含 TTFT+尾部/还原公式/stall_ms is None 分支条件/与 NVIDIA-vLLM 基准口径不扣除沉默的显式区隔)+ 配置表两键(值域/缺省/守卫/三层调整接口)+ 能力表"中段沉默对冲(可选,超业界现状能力)"行。
    3. .env.example:对冲两行注释后加 # LLM__STREAM__STALL_THRESHOLD_MS=1000 与 # LLM__HEDGE__STALL_AFTER_S=(缺省关闭,语义与成本说明)。
    4. research-wiki/ARCHITECTURE.md:口径段(generation_ms 净口径 + 归因两字段 + 换算唯一处纪律)与对冲触发段(两阈值独立生效/两阶段触发窗/与看门狗交互)登记。
    5. research-wiki/ROADMAP.md:1.3.8 里程碑登记。
    6. research-wiki/findings/2026-09-11-streaming-stall-validation.md:红绿证据、命令与退出码、豁免索引(≤300 行);wiki 注册本计划与 findings。
  • 证据出口:tests/outputs/138/t5/lint.log + 审查/验证纪要不进 outputs(research-wiki/reviews/ 按既有惯例)。
  • 验收命令:make lint + make ci(只读验证);版本 bump/tag/构建/上传 registry 不在本计划内,按 CLAUDE.md §4.4.1 另行执行。
  • 回滚点:文档提交独立 revert 即可。

5. 测试矩阵 → 任务映射(汇总)

批次 内容 任务 落点
A 端口加参 + 原语 + stall 纯函数(12 用例) T1 test_openai_compat.py / test_types.py
B 净口径记账(6 用例) T2 test_types.py / test_retry.py
C 遥测两列(冻结扩 + 4 新用例 + 3 负例核对) T3 test_telemetry.py / test_hedge.py / test_ports.py
D 中段对冲编排(8 用例)+ 配置守卫(6 用例) T4 test_stall_hedge.py(新)/ test_config.py
E 默认关闭回归门(不改一行既有断言) T4 门 tests/unit + tests/contracts 全套件

设施复用核对(动手前必读):test_retry.py:120-127 FakeSleep 只记录不推进时钟(推进须用例自带闭包);test_hedge.py:41-69 事件驱动假 transport(content_event 身份逐次记录,同款扩展);test_config.py:31-33 loguru WARNING 捕获 fixture(caplog 抓不到 loguru);取消窗口用 entered Event 范式(test_retry.py:79-89);真实 loop 钟阈值 0.05s、容差 4–10×(test_streaming.py 既有范式,不标 slow);test_telemetry.py:53-99 冻结清单的扩展属"先红"手段(先改清单 → schema 未改即红)。

6. 风险节与验收门

风险 纪律
钟混用:测试注入 FakeClock 时真实 transport 用了假钟,或沉默序列与墙钟异钟 N4 钉死:沉默序列(transport time.monotonic)与墙钟(retry 注入 now,生产 = time.monotonic)生产同钟;FakeClock 用例一律配自控 stall 值的假 transport,真实 transport 测试不注入假钟;对冲触发只用 loop 相对时长,绝不读注入 now(1.3.7 §4.1)
取消穿透回归:两阶段重构动 _past_hedge_window,1.3.6/1.3.7 既有取消语义被破坏 既有取消/对冲/看门狗测试不得改语义只许扩展;waiter 收口 + cancelling() 穿透逐字保留;批次 D 外部取消用例 + 批次 E 全绿为双门
遥测 DDL 对既有实例不幂等 新列可空无默认追加末尾;auto_migrate 先探测后 ALTER(SQLite duplicate column 竞态已有处理);批次 C 迁移幂等用例(二次初始化)为门;manual 档裁剪 + 点名告警同源(missing_columns_warning 由常量派生,零编辑)
端口签名同步遗漏(1.3.7 T1 教训) §2.2/§2.3 两表为真实 grep 全量结果;T1/T3 全套件绿为门(遗漏 = TypeError/装配闸 ValueError 必红);不许凭记忆补点,执行时先重跑同一条 grep 复核
换算漂移:多处各自取整/各自减沉默 N8:取整与净额计算全库只 retry 层一处;_record_generation/_register_hedge 共用同一内部换算;遥测列取 INTEGER 与之一致
误触发:chunk 与超时同一 pass 到达 N3① 复检 is_set();批次 D test_stall_recheck_prevents_false_trigger 为门
触发率成本:实测沉默 >20s 占 19.0%,远高于首 token 对冲 默认必须关闭;CHANGELOG/README 强制措辞"对冲 = 用配额换延迟";守卫②把"对冲窗永不开"的配置在装配期直接拒掉
反馈环:计量阈值与对冲阈值自适应耦合 不做自适应(设计 §8-B);两阈值皆绝对值,文档明写
emit_attempt 默认 None 掩盖 chat 路径漏传 批次 C 落行断言(chat 成功行两列恰为传入值)为门;取舍理由已录 §3.4

验收门(合并前,全部 conda run -n PolyGateway 禁接管道):

  1. make lint(ruff --fix + import-linter;ports/types/errors 不 import 实现的铁律由契约执法)。
  2. 全套件:pytest tests/unit tests/contracts tests/integration -q(integration 中 PG/Redis 用例按其既有标记;默认不含 slow)。
  3. slow 交集子集(CLAUDE.md §4.4.1 第 4 步,按 diff 交集):本 diff 触及 retry(结算/取消/编排)与 telemetry → Redis 时间语义变体(tests/integration/test_redis_governance_time.py 等与取消/结算交集的 slow 变体)+ 网关冒烟(tests/e2e/test_smoke_gateway.py,公开入口 complete 签名变更属交集)。test_thinking_live.py 全模型矩阵不跑(本 diff 未动 thinking.py/能力注册表;复用 1.3.7 有效矩阵证据并如实登记)。
  4. T5 前:requesting-code-review(整分支)+ verification-before-completion(全新上下文 verifier subagent,前台)。

7. 阻塞矩阵与交接

触发条件 处置
需要新增设计 §9 之外的公共键/端口方法/遥测列/异常类 停下上报(边界之外即未批准)
asyncio.current_task().cancelling() 语义在目标版本不符 3.12 语义同 1.3.6/1.3.7 已验证路径;若实测不符,改"取消标志位置于 _attempt_hedged 局部"方案并记 findings,不得吞取消
复检防误触发用例在 CI 抖动 只断"不触发 + attempts==1"结果,不断精确时序;仍不稳则调宽阈值比(0.45×阈值置位)并记 findings,不得删复检断言
想顺手让对冲路携带 content_event(梯次) 不做(N3③:v1 单路;对冲路恒 None)
想把看门狗掐死改成不喂熔断 / 把输家取消改成喂熔断 不改任一(N9 不对称声明:前者 1.3.6 起既有语义,后者 1.3.7 已批准语义);运维"挂起率"统计须知此不对称,落 README
想给 LLMResponse 加顶层 stall 字段 不加(B2 裁决 (b):CallStats 已承载赢家口径,两处平铺必漂移)
发现 embedding/OCR 也想用中段对冲 不加(非目标 E);B4 warning 已覆盖误配场景;登记为后续 issue
PG 实库迁移用例无沙箱可跑 记 findings 豁免索引,发布门 slow 交集时补跑;不得删批次 C 的 SQLite 幂等用例顶替

交接物:5 个提交(T0 文档并入首个提交或独立,由 commit skill 裁定)、1 份 findings、CHANGELOG 未发布段。版本号 bump、tag、构建、上传 registry 与 wiki 同步不在本计划内,按 CLAUDE.md §4.4.1 另行执行(发布时按 §6 验收门 3 补 slow 交集)。

8. 自审

检查 结论
路径/行号/签名是否可执行无 TBD 是——全部接入点本会话现读:ports.py:56-66/306-344、types.py:296-324/328-388/560-586/590-700、openai_compat.py:427-473/553-616/660-705、retry.py:239-300/304-400/371-562/665-700、config.py:57/192-196/382-392/398-427/745-769/772-849/854-910、client.py:217-310/485-526、middleware/telemetry.py:199-236/396-665、telemetry/schema.py:22-215/323-372、§2.2/§2.3 全部测试落点
同步清单是否真实 grep 是——§2.2(11 实现 + 11 调用点)与 §2.3(8 类落点)均为 2026-09-11 本会话实跑结果,非凭记忆;执行时重跑复核已写进 §6
先失败后通过证据点 是——T1 批次 A(TypeError/ImportError)、T2 批次 B(字段缺失/仍得墙钟)、T3 批次 C(冻结清单先扩即红/装配闸)、T4 批次 D(键未识/hedges==0/超时)均先红;证据出口 tests/outputs/138/t<N>/ 逐卡列出
设计保真 是——净口径/排除条款/换算唯一处/两阶段触发窗/N3 并发纪律/N9 不对称/B2 裁决 (b)/B4 操作化均逐字落自设计;无另行发明语义
取消与降级铁律 取消穿透路径逐字保留既有收口;准入失败静默;后端不可用冒泡;遥测写失败降级;DDL 幂等;无 shield/后台任务
反 gold-plating 四分类/熔断/429/Lua/streaming.py/embedding-OCR 链路/LLMResponse 顶层字段/梯次对冲/自适应阈值一律不碰;recorder **fields 零改动;约 50 处 emit_attempt 调用点零改动
跨任务签名一致 content_event(T1 端口 → T4 接线)、stall 三元组 sink(T2 定义 → T4 赢家断言)、stall_after_s(config 定义 → client 透传 → RetryMW 消费)、stall_threshold_ms(SourceConfig 字段 → 装配 replace 分发 → transport 读)四处接缝均在 §3 写出实际代码
行数 约 400 行 ≤1000;表格/伪代码优先;无超 15 行代码块
残余诚实标注 中段对冲无业界公开先例(调研 §A3,CHANGELOG 必写);generation_ms 语义突变为人类"窗口期最小代价"裁决放行的例外(披露义务在 T5);复检竞速窗口只能收窄不能归零(N3① 已是设计批准的最强形态)