# 给 QMT 侧：补 `ack_seq` 处理（协议 §4.5）

## 现象

PMS 每 2 秒发一次 `ack_seq`，QMT 侧回：

```json
{"instruction_id": null, "code": "BAD_PARAM", "reason": "unknown type ack_seq", "retryable": false}
```

定位到 `handlers.py` 的 `handle()`：类型分发链里有 `hello` / `ping` / `place_order` /
`cancel_order` / `query_positions` / `query_funds` / `query_orders`，**唯独没有 `ack_seq`**，
所以落到了最后一行的兜底：

```python
self._reject_raw(envelope, 'BAD_PARAM', f'unknown type {msg_type}', False)
```

对过一遍：我方下行 8 个类型，贵方实现了 7 个，只差这一个。

## 为什么会漏

多半是协议文档的锅，不是实现的锅。`QMT_WS_PROTOCOL.md` 的版本历史表里，
**V1.0 定稿那行排在 V0.9.2 上面**，而 `ack_seq` 恰恰是 V0.9.2 才补进来的
（「新增下行消息 ack_seq 累积确认（§4.5）」）。从上往下读到「V1.0 定稿」就停的话，
底下引入 `ack_seq` 的那行会被跳过去。PMS 侧会把这两行顺序修正，条款不动。

## 后果（已定位，非猜测）

不处理会形成自激循环：

```
PMS 发 ack_seq → QMT 回 reject → reject 自身带 seq → PMS 水位推进
   → 2 秒后 PMS 又要 ack → 再被 reject → ...
```

实测 2 秒一条，十几分钟烧掉 800 多个 seq。通道显示 ONLINE，实际只在刷 reject。

更实质的影响在贵方：§6.1 规定「QMT 收到 `ack_seq{seq: N}` 后可清理 `seq ≤ N` 的消息」。
收不到 ack，**Redis 里的上行消息只涨不清**。联调期 Redis 不设过期（双方已确认），
这部分会一直堆着。

## 补法

`handlers.py` 的 `handle()` 里，在 `if msg_type == 'ping':` 附近加一个分支：

```python
if msg_type == 'ack_seq':
    seq = int(payload.get('seq') or 0)
    if seq > 0:
        self.store.ack(seq)          # 清理 seq <= N；方法名按贵方 store 的实际接口来
    return                           # 不需要回任何消息
```

三点提醒：

1. **`ack_seq` 不需要回消息。** 它是单向通知，回 `ack` 或 `pong` 都会让 PMS 侧多一条
   无主上行。
2. **累积语义。** 收到 `{seq: 10450}` 表示 `seq ≤ 10450` 全部已落库，可一次性清理，
   不是只清这一条。
3. **§6.1 的 7 天下限仍要守。** 「已确认的消息也至少保留 7 天」——防 PMS 侧库回滚后
   无从追溯。所以是「可清理」不是「立即删」，按贵方存储策略取舍。

## PMS 侧已做的临时处理

PMS 已加自动降级：收到 reason 里含 `ack_seq` 的协议级 reject 就**停发 `ack_seq`**，
打断循环。水位照常落库，重连补发靠 `hello.last_seq`（§4.5 原话：「断线重连时以
`hello.last_seq` 为准，`ack_seq` 只用于让 QMT 及时释放存储」），**不丢成交**。

标志位每次重连重置——贵方补上之后，PMS 重连即自动恢复发送，不需要通知我们改配置。

## 顺带确认的几件事

- **签名完全互通。** `crypto1.py` 的 `compact_payload_json` / `build_canonical` 与 PMS 侧
  逐字一致，双向验签实测通过，这一层可以划掉了。
- **`new_nonce()` 已修。** 上一版是 `token_hex(8)`（8 字节），现在是 `token_hex(16)`，
  符合 §2.2。
- **时间窗。** `handlers.py` 对我方每条消息做 ±30 秒检查（超了回 TS_SKEW）。这是对的，
  但两机时钟漂开会让 `place_order` 直接被拒，现象是「下单没反应」，很难往时钟上想。
  PMS 侧已把实测偏差显示出来，建议两边都确认 NTP 在跑。
- **`pong` 带 seq** 是符合 §5「所有上行消息带 seq」的，实现没问题。只是按 §6.1 它也会
  进 Redis 并在重连时补发——5 秒一个，一天 1.7 万条纯 `{}`。要不要给 `pong` 免掉 seq，
  是个可以聊的口径问题，不急。
