@twinsearth/dsh-master 3.2.10 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/client.js +50 -0
- package/dist-host/contract/client.js +7 -0
- package/dist-host/contract/constants.js +16 -2
- package/dist-host/contract/index.js +1 -0
- package/dist-host/contract/relay.js +239 -0
- package/dist-host/cost/attribution.js +183 -0
- package/dist-host/cost/audit.js +163 -0
- package/dist-host/cost/budget.js +211 -0
- package/dist-host/cost/bus.js +95 -0
- package/dist-host/cost/cert-chain.js +100 -0
- package/dist-host/cost/experiment.js +132 -0
- package/dist-host/cost/fang-relay.js +329 -0
- package/dist-host/cost/gate.js +189 -0
- package/dist-host/cost/host-surface.js +80 -0
- package/dist-host/cost/index.js +345 -0
- package/dist-host/cost/ledger-report.js +193 -0
- package/dist-host/cost/ledger.js +401 -0
- package/dist-host/cost/metrics.js +198 -0
- package/dist-host/cost/milestones.js +209 -0
- package/dist-host/cost/modules/index.js +5 -0
- package/dist-host/cost/modules/manifests.js +130 -0
- package/dist-host/cost/policy.js +276 -0
- package/dist-host/cost/report.js +156 -0
- package/dist-host/cost/reuse-report.js +128 -0
- package/dist-host/cost/rsi.js +367 -0
- package/dist-host/cost/toolface.js +147 -0
- package/dist-host/cost/types.js +31 -0
- package/dist-host/kernel.js +28 -2
- package/dist-host/plugins/cost-optimizer.js +329 -27
- package/dist-host/plugins/superagent-router.js +104 -1
- package/dist-host/superagent/types.js +8 -0
- package/dist-worker/agent-loop.js +449 -7
- package/dist-worker/baseline-fit.js +66 -0
- package/dist-worker/budget-guard.js +93 -0
- package/dist-worker/ccs.js +99 -0
- package/dist-worker/cost-hooks.js +115 -0
- package/dist-worker/cost-modules.js +330 -0
- package/dist-worker/cost-spi.js +3 -0
- package/dist-worker/edg.js +372 -0
- package/dist-worker/gdr.js +159 -0
- package/dist-worker/index.js +80 -2
- package/dist-worker/llm-http.js +20 -0
- package/dist-worker/sink-http.js +1 -0
- package/dist-worker/spill.js +115 -0
- package/dist-worker/tool-batch.js +66 -0
- package/dist-worker/tool-compact.js +275 -0
- package/dist-worker/tool-disclosure.js +90 -0
- package/dist-worker/tools.js +16 -0
- package/docs/ARCHITECTURE.md +24 -0
- package/docs/CHANGELOG.md +66 -0
- package/docs/DESIGN-v3.3.0-cost-optimization.md +1058 -0
- package/docs/EXPERIMENT-M5-fang-relay.md +117 -0
- package/docs/EXPERIMENT-M5-real-ab.md +87 -0
- package/docs/THIRD-PARTY.md +87 -0
- package/package.json +2 -2
- package/src/contract/client.js +7 -0
- package/src/contract/constants.js +17 -2
- package/src/contract/index.js +1 -0
- package/src/contract/relay.js +223 -0
- package/src/cost/attribution.ts +186 -0
- package/src/cost/audit.ts +170 -0
- package/src/cost/budget.ts +226 -0
- package/src/cost/bus.ts +97 -0
- package/src/cost/cert-chain.ts +102 -0
- package/src/cost/experiment.ts +136 -0
- package/src/cost/fang-relay.ts +318 -0
- package/src/cost/gate.ts +191 -0
- package/src/cost/host-surface.ts +97 -0
- package/src/cost/index.ts +348 -0
- package/src/cost/ledger-report.ts +186 -0
- package/src/cost/ledger.ts +385 -0
- package/src/cost/metrics.ts +193 -0
- package/src/cost/milestones.ts +206 -0
- package/src/cost/modules/index.ts +13 -0
- package/src/cost/modules/manifests.ts +129 -0
- package/src/cost/policy.ts +276 -0
- package/src/cost/report.ts +158 -0
- package/src/cost/reuse-report.ts +131 -0
- package/src/cost/rsi.ts +364 -0
- package/src/cost/toolface.ts +151 -0
- package/src/cost/types.ts +298 -0
- package/src/kernel.ts +27 -2
- package/src/plugins/cost-optimizer.ts +313 -26
- package/src/plugins/superagent-router.ts +96 -1
- package/src/superagent/types.ts +70 -0
- package/worker/agent-loop.ts +444 -7
- package/worker/baseline-fit.ts +80 -0
- package/worker/budget-guard.ts +156 -0
- package/worker/ccs.ts +202 -0
- package/worker/cost-hooks.ts +133 -0
- package/worker/cost-modules.ts +356 -0
- package/worker/cost-spi.ts +283 -0
- package/worker/edg.ts +424 -0
- package/worker/gdr.ts +233 -0
- package/worker/index.ts +84 -3
- package/worker/llm-http.ts +18 -0
- package/worker/sink-http.ts +5 -2
- package/worker/spill.ts +133 -0
- package/worker/tool-batch.ts +124 -0
- package/worker/tool-compact.ts +317 -0
- package/worker/tool-disclosure.ts +131 -0
- package/worker/tools.ts +553 -531
- package/worker/types.ts +191 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# M5 · 瓮城回流接口实测报告(contract 1.1.0 / 房主 3.3.1)
|
|
2
|
+
|
|
3
|
+
> 结论:**M5 达标(done)**,但证据的**对端身份是 `contract-server`**(瓮城侧契约 SDK 的独立进程),
|
|
4
|
+
> 不是 `dsh-fang-bundle` 本体。这个区别写在里程碑结果行的 `notes` 里,不隐藏。
|
|
5
|
+
>
|
|
6
|
+
> 日期:2026-11-13 · 环境:Windows / Node 26.13.0 / 单机双进程
|
|
7
|
+
|
|
8
|
+
## 1. 为什么需要这一轮
|
|
9
|
+
|
|
10
|
+
3.3.0 的 M5 只差一件事:`与瓮城回流对接`。当时的实现是**硬编码**
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
relay: { target: 'fang', available: false, reason: 'fang-channel-not-ready' }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
接口位在,但没有协议、没有投递、没有回执。contract 1.0.0 的方法表里也确实没有任何
|
|
17
|
+
回流方法(只有 `fang.fork.*` / `fang.decision.*` / `fang.session.list` / `fang.orchestrator.status`),
|
|
18
|
+
所以“接上”只能先把接口版本升上去。
|
|
19
|
+
|
|
20
|
+
## 2. 这一轮改了什么
|
|
21
|
+
|
|
22
|
+
### 2.1 接口包 `@twinsearth/dsh-fang-contract@1.1.0`
|
|
23
|
+
|
|
24
|
+
| 变化 | 内容 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| 新方法 | `fang.relay.publish`(批量投递,按 `event.id` 幂等,返回回执 + 游标)、`fang.relay.status`(对端自报 peer/cursor/累计计数) |
|
|
27
|
+
| 新能力 | `event-relay` |
|
|
28
|
+
| 参考实现 | `createRelayStore` / `validateRelayEvents` / `relayHandlers` / `createRelayContractServer` |
|
|
29
|
+
| 客户端 | `publishRelay()` / `relayStatus()` |
|
|
30
|
+
| 传输加固 | 逐调用 `timeoutMs`/`meta` 透传、`httpHeaders` 透传、HTTP 定时器 `unref`、file 回执/请求文件有界重试清理 |
|
|
31
|
+
| 兼容 | 只升 MINOR;1.0 对端不会宣告 1.1 方法,`negotiate()` 按公共 minor 自动裁剪 |
|
|
32
|
+
|
|
33
|
+
发布核验:npm `1.1.0` = `latest`,下载包与本地 tarball **字节一致**
|
|
34
|
+
(sha256 `e5224dc682b7a66247cd100d47492047b081acf233fd1f7daac4fab6753d5bd4`,11 个文件)。
|
|
35
|
+
仓库 main 已推到 `c106053d`(tree 与本地一致)。
|
|
36
|
+
|
|
37
|
+
### 2.2 房主侧
|
|
38
|
+
|
|
39
|
+
- `src/contract/` 内嵌副本同步到 1.1.0,与已发布包**逐字节相同**(`test/contract-conformance.test.mjs` 守卫);
|
|
40
|
+
- 新增 `src/cost/fang-relay.ts`:`probe()` 判定对端是否真的提供回流;`enqueue()/flush()`
|
|
41
|
+
批量投递且**只认回执**;失败退回队列并转 `publish-failed:*`;
|
|
42
|
+
- `cost-optimizer` / `superagent-router` / `createCostSystem()` 全部改为读真实状态;
|
|
43
|
+
- M5 判定收紧为 `available + peer 白名单 + events ≥ 3 + 非自环`。
|
|
44
|
+
|
|
45
|
+
## 3. 实测方法(可复现)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run build
|
|
49
|
+
node scripts/fang-relay-probe.mjs --events 6 --out relay-evidence.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
脚本行为:
|
|
53
|
+
|
|
54
|
+
1. 起一个**独立子进程**(`--serve`),里面跑**已发布的 npm 包**
|
|
55
|
+
`@twinsearth/dsh-fang-contract@1.1.0` 的 `createRelayContractServer`,
|
|
56
|
+
以 HTTP 暴露 `/fang/api/contract/{hello,invoke,health}`(Bearer token,不裸奔);
|
|
57
|
+
2. 父进程用仓库内嵌副本作客户端,`probe()` → 入队 6 条事件(三类事件混合)→ `flush()`;
|
|
58
|
+
3. 只把对端回执当证据;`sameProcess` 由两侧 pid 比较得出;
|
|
59
|
+
4. 证据 JSON 顶层就是 `{ ok, fangRelay, detail }`,可直接喂给 `scripts/milestones.mjs --input`。
|
|
60
|
+
|
|
61
|
+
## 4. 实测结果
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"ok": true,
|
|
66
|
+
"fangRelay": {
|
|
67
|
+
"available": true,
|
|
68
|
+
"peer": "contract-server",
|
|
69
|
+
"events": 6,
|
|
70
|
+
"sameProcess": false,
|
|
71
|
+
"transport": "http",
|
|
72
|
+
"contractVersion": "1.1.0",
|
|
73
|
+
"reason": null,
|
|
74
|
+
"cursor": 6,
|
|
75
|
+
"failed": 0,
|
|
76
|
+
"dropped": 0
|
|
77
|
+
},
|
|
78
|
+
"detail": {
|
|
79
|
+
"probePid": 13340,
|
|
80
|
+
"peerPid": 4476,
|
|
81
|
+
"endpoint": "http://127.0.0.1:58328",
|
|
82
|
+
"batches": [{ "ok": true, "delivered": 6, "duplicates": 0, "sent": 6, "queued": 0, "cursor": 6, "batchId": "relay-1" }]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
里程碑输出:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
M5 │ 实现 ✔ │ 依赖 ✔ │ 效果 verified │ 状态 done │ 回流对端 'contract-server·6条'
|
|
91
|
+
M5 说明: peer=contract-server:契约对端独立进程实测,dsh-fang-bundle 采用 contract 1.1 后应变为 peer=dsh-fang-bundle
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 5. 能证明 / 不能证明
|
|
95
|
+
|
|
96
|
+
**能证明**
|
|
97
|
+
|
|
98
|
+
- contract 1.1 的回流协议在**两个真实进程**之间跑通:hello 协商 → 方法裁剪 → 投递 → 回执 → 游标;
|
|
99
|
+
- 幂等成立:同 id 重投 `accepted:0 / duplicates>0`,游标不倒退;
|
|
100
|
+
- 失败路径不伪装:客户端不可用 / 对端 1.0 / hello 失败 / status 失败 / publish 失败,
|
|
101
|
+
五种情况分别有具名原因,且失败批次会退回队列并计入 `failed`(有单元测试);
|
|
102
|
+
- 房主内嵌副本与已发布包无漂移(逐字节 + 双向互操作测试)。
|
|
103
|
+
|
|
104
|
+
**不能证明**
|
|
105
|
+
|
|
106
|
+
- 不能说“已与真实 dsh-Fang bundle 完成对接”:bundle 尚未依赖 contract 1.1,也尚未挂
|
|
107
|
+
`relayHandlers`。等 bundle 采用 1.1 后,`peer` 自报值会变成 `dsh-fang-bundle`,
|
|
108
|
+
M5 的 `notes` 会自动消失(代码里写死的是白名单,不需要改里程碑表);
|
|
109
|
+
- 不能证明生产环境的网络/鉴权拓扑:本轮是 127.0.0.1 + Bearer token 的本机双进程。
|
|
110
|
+
|
|
111
|
+
## 6. 复现注意事项
|
|
112
|
+
|
|
113
|
+
- 必须 `npm run build` 后跑:脚本 import 的是 `dist-host/`;
|
|
114
|
+
- 子进程用的是 `node_modules` 里**已发布**的包,父进程用 `src/contract/`,两侧故意不同源,
|
|
115
|
+
才能真正测出“已发布包 ↔ 内嵌副本”的互操作;
|
|
116
|
+
- 证据文件不要提交进仓库(G6 零残留门禁会检查 `rsi-report.json` 一类产物);
|
|
117
|
+
- Windows 上子进程退出由 `SIGTERM` 处理,若端口被占用会直接报 ready 超时。
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# M5 真实对照实验报告(3.3.0-dev)
|
|
2
|
+
|
|
3
|
+
> 生成时间:2026-11-13 08:5x(Asia/Shanghai)
|
|
4
|
+
> 运行命令:`node scripts/fork-experiment.mjs --real --tasks 20 --model deepseek-chat`
|
|
5
|
+
> 原始结果:`E:\GPT\.tmp\real-ab.json`(本地,不入库)
|
|
6
|
+
|
|
7
|
+
## 1. 这次跑的是什么
|
|
8
|
+
|
|
9
|
+
| 项 | 值 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| provider | DeepSeek 官方 API(`https://api.deepseek.com`,模型 `deepseek-chat`) |
|
|
12
|
+
| Worker | **本仓库 `worker/` 的真实代码路径**:真实工具注册表、真实 A1 紧凑化、真实 A3 批量、真实 B1/B2/B3 |
|
|
13
|
+
| 对照分组 | `control` = 3.2.x 行为(`compactMode:'off'`、无复用);`enforce` = `compactMode:'enforce'` + `ccs.mode:'enforce'`(白名单) |
|
|
14
|
+
| 样本量 | 每组 **20** 个任务(G5 要求 ≥10) |
|
|
15
|
+
| 票据来源 | **合成**(脚本生成的任务描述与工作区,不是飞书/GitHub 真 ticket) |
|
|
16
|
+
| harness 标签 | `real-provider-real-worker`(`qualifiesForGate: true`) |
|
|
17
|
+
| 是否安装到任何 profile | **否**(未安装、未升级、未改 profile 配置) |
|
|
18
|
+
|
|
19
|
+
## 2. 结果(同表,两组并列)
|
|
20
|
+
|
|
21
|
+
| 组 | 样本 | tokens/task | 成功率 | 耗时中位数 |
|
|
22
|
+
|---|---|---|---|---|
|
|
23
|
+
| control | 20 | **6263** | 100% | 3200 ms |
|
|
24
|
+
| enforce | 20 | **3906.75** | 100% | 3274 ms |
|
|
25
|
+
|
|
26
|
+
- **tokens/task 下降 −2356.25(−37.6%)**
|
|
27
|
+
- **成功率差值 0pp**(两组均 20/20 成功,未触及 §10.1 的 2pp 降级红线)
|
|
28
|
+
|
|
29
|
+
## 3. 拆开看这两项省在哪
|
|
30
|
+
|
|
31
|
+
| 链路 | 实测 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| **A1 回执压缩**(单价项) | 20/20 任务发生压缩,估算节省 **53,549 tokens** |
|
|
34
|
+
| **B2/B3 决策复用**(次数项) | 复用尝试 **20** · 命中 **20**(100%)· 跳过模型调用 **20** · 真实派发 **20** · 失败回落 **0** |
|
|
35
|
+
|
|
36
|
+
口径说明:
|
|
37
|
+
- A1 的 token 是**估算值**(`chars/4`),报表里与实测分开列;
|
|
38
|
+
- B 的节省按「跳过 1 次模型回调 × 该步基线下单」计,与 A1 的单价下降**分列不合并**(§8 口径)。
|
|
39
|
+
|
|
40
|
+
## 4. 这些数字能证明什么、不能证明什么
|
|
41
|
+
|
|
42
|
+
**能证明**
|
|
43
|
+
- 在真实 provider + 真实 Worker 代码路径下,A1+B 组合让 tokens/task 下降约 **37.6%**;
|
|
44
|
+
- 复用命中不会牺牲成功率(两组都是 100%);
|
|
45
|
+
- 派发失败自动回退路径没有被触发(0 次),说明白名单内的边在这批语料上是稳定的。
|
|
46
|
+
|
|
47
|
+
**不能证明**
|
|
48
|
+
- **不代表真实 ticket 上的收益**:任务描述与工作区是合成的,真实工单的规模/形态差异可能显著改变比例;
|
|
49
|
+
- **不是安装态实测**:本次未安装到任何 profile,也没有在 18000 端口那个 DSH 实例里跑——该实例的 profile(`E:\DSH\.dsh\profiles\desktop`)**没有安装 dsh-master**(我核对过:`node_modules\@twinsearth\dsh-master` 不存在,`/dsh-master/api/*` 一律 404);
|
|
50
|
+
- **不含 §8 的净亏检查**:governance 成本(EDG 作者步骤是模板来源、作者成本 0)在这批任务里没有真实开销。
|
|
51
|
+
|
|
52
|
+
## 5. 复现步骤
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 需要真实 provider 凭据(本文件不记录、不入库)
|
|
56
|
+
export DSH_MASTER_LLM_BASE_URL=https://api.deepseek.com
|
|
57
|
+
export DSH_MASTER_LLM_API_KEY=<your-key>
|
|
58
|
+
export DSH_MASTER_LLM_MODEL=deepseek-chat
|
|
59
|
+
|
|
60
|
+
node scripts/fork-experiment.mjs --real --tasks 20 --out /tmp/real-ab.json --json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`--real` 与默认 dry-run 的区别写在脚本头部;dry-run 的 `harness=local-dry-run` 会被
|
|
64
|
+
`evaluateForkExperiment()` 判 `harness-not-real` —— **dry-run 数据无法冒充 G5 证据**。
|
|
65
|
+
|
|
66
|
+
## 6. 门禁与里程碑
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
[gate] ✔ G1 check / ✔ G2 build / ✔ G3 test / ✔ G4 覆盖 13/13
|
|
70
|
+
✔ G5 control 6263 → enforce 3906.75 tokens/task(20/20 组)
|
|
71
|
+
✔ G6 零残留 ✖ G7 未授权
|
|
72
|
+
bootstrapOk=true ok=false(只剩发布授权)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
[milestones] 实现 5/5 · 达标 4/5
|
|
77
|
+
M1 done · M2 done · M3 done · M4 done
|
|
78
|
+
M5 implemented-effect-unmeasured(原因:fang-relay-out-of-scope)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
M5 当时唯一未达标项是「与瓮城回流对接」—— 该通道当时没有协议实现,
|
|
82
|
+
接口位显式返回 `{ available:false, reason:'fang-channel-not-ready' }`,不伪装成功。
|
|
83
|
+
|
|
84
|
+
> **后续(3.3.1 已收口)**:contract 1.1.0 新增 `fang.relay.publish` / `fang.relay.status`,
|
|
85
|
+
> 房主侧新增 `src/cost/fang-relay.ts` 真实投递,并用独立进程对端拿到 6 条回执;
|
|
86
|
+
> M5 现在 `done`(`peer=contract-server`)。详见
|
|
87
|
+
> [`EXPERIMENT-M5-fang-relay.md`](EXPERIMENT-M5-fang-relay.md)。
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# 第三方参考实现与移植边界
|
|
2
|
+
|
|
3
|
+
> 适用范围:整个 `dsh-Master` 仓库(含 `src/`、`worker/`、`client.js`、`docs/`)。
|
|
4
|
+
> 本文件是**约束文件**,不是致谢清单:它规定我们「可以借鉴什么、不可以复制什么」,
|
|
5
|
+
> 并由 `test/license-provenance.test.mjs` 自动守卫。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 参考项目
|
|
10
|
+
|
|
11
|
+
| 项目 | 许可证 | 层次 | 在本设计中的角色 |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| [`T-Auto/dsh-ops`](https://github.com/T-Auto/dsh-ops) | MIT | 工具层(I/O 单价) | **方法参考**:紧凑工具输出、按需披露工具、批量调用可以降低单次交互 Token |
|
|
14
|
+
| [`YuhuaJiang2002/HarnessMM`](https://github.com/YuhuaJiang2002/HarnessMM) | Apache-2.0 | 决策层(推理次数) | **方法参考**:EDG / GDR / CCS / RSI 可在模型—工具边界跳过冗余推理;两本账(执行节省 vs 治理成本)分开核算 |
|
|
15
|
+
|
|
16
|
+
论文:HarnessMM,arXiv[:2610.32813](https://arxiv.org/abs/2610.32813)。
|
|
17
|
+
|
|
18
|
+
两者解决同一问题的不同侧面:`dsh-ops` 让**每次工具调用更便宜**,`HarnessMM` 让**需要发生的推理次数更少**。
|
|
19
|
+
本项目的 3.3.0 把两者拆成 A(工具面)/ B(决策复用)两条链路,用一套计量与治理系统联合起来。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. 移植边界(硬性)
|
|
24
|
+
|
|
25
|
+
> **只移植协议语义与问题分解方式;实现全部自研;不复制第三方代码。**
|
|
26
|
+
|
|
27
|
+
| 允许 | 禁止 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| 阅读论文/文档,提炼**问题分解**(如「单价 vs 次数」)与**协议语义**(如「守卫命中则复用已解决决策」) | 复制源码、函数体、注释、常量表、正则字面量集合、数据结构布局 |
|
|
30
|
+
| 重新设计并**自研实现**同等能力,接口与命名按本项目风格 | 引入第三方包作为运行时依赖来「代劳」 |
|
|
31
|
+
| 在文档/代码注释中**引用**项目名、论文号、许可证名 | 在源码里带上第三方版权头 / 许可证正文 / 出处不明的大段代码 |
|
|
32
|
+
| 使用公开可用的**实测数字**做对标(标注来源) | 把第三方基准数字当作本项目实测结果对外宣称 |
|
|
33
|
+
|
|
34
|
+
**判定标准**:如果删掉参考项目的仓库,我们的实现应当仍能独立读懂、独立测试、独立演进——
|
|
35
|
+
不能存在「只有对照原项目才看得懂」的代码。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 3. 当前实现来源核对
|
|
40
|
+
|
|
41
|
+
| 模块 | 来源 | 说明 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| 系统层 `ledger` / `budget` / `policy` / `audit` / `attribution` / `bus` | **自研** | 通用工程结构(计量、闸门、灰度、哈希链、归因、事件)。“两本账”这一核算口径来自 HarnessMM 论文的**原则**,实现与数据结构均为本项目自写。 |
|
|
44
|
+
| A1 `tool-compact` | **自研** | 只针对本项目自有工具(`fs_read_file` / `fs_list_dir` / `fs_write_file` / `exec_shell_in_vm`)的输出形状做无损压缩;与 `dsh-ops` 的 Rust 工具、`ops_*` 命名、PTC 预设**无代码关系**。 |
|
|
45
|
+
| A2 / A3 | 未实现 | 将来按同一 SPI 自研;**不引入** `dsh-ops` 的 `tools_addition` 运行时与 Rust 二进制。 |
|
|
46
|
+
| B1–B4 | 未实现(仅 SPI 类型) | 将来按 EDG/GDR/CCS/RSI 的**语义**自研;**不复制**其图结构定义、证书格式或 RSI 训练脚本。 |
|
|
47
|
+
| 四切点 `cost-hooks` + 模块 SPI | **自研** | 本项目 Agent Loop 的既有边界,非第三方 API。 |
|
|
48
|
+
|
|
49
|
+
**基准数字**(`HARNESSMM_BENCHMARK`)是论文公开数值,仅用于对标与单测断言常量,
|
|
50
|
+
不参与任何对外「我们省了多少」的宣称;本项目实测值由 `ledger` 产生并与基准并列展示。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 4. 编写 B1–B4 时的检查清单
|
|
55
|
+
|
|
56
|
+
新增决策复用能力前,逐条确认:
|
|
57
|
+
|
|
58
|
+
1. [ ] 结构体/状态机是从**问题**出发设计,而不是照抄参考项目的字段名与层级?
|
|
59
|
+
2. [ ] 守卫条件、证书、图的序列化格式均为本项目自定义(不兼容、不消费第三方产物)?
|
|
60
|
+
3. [ ] 没有新增运行时依赖;没有 vendored 代码目录;没有 Rust/二进制产物?
|
|
61
|
+
4. [ ] 每个新文件带 `SPDX-License-Identifier: AGPL-3.0-or-later` 头?
|
|
62
|
+
5. [ ] 若某段实现确实**必须**基于第三方代码(当前无此情况),先在 PR 中单独说明:
|
|
63
|
+
来源 URL、许可证、复制的具体范围,并在 `NOTICE`/`THIRD-PARTY` 与文件头保留原始声明——
|
|
64
|
+
**未经此步骤不得合入**。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 5. 许可证
|
|
69
|
+
|
|
70
|
+
- 本项目:`AGPL-3.0-or-later`(见 `LICENSE`、`LICENSE-CORE`)。
|
|
71
|
+
- MIT 与 Apache-2.0 均为宽松许可证,与本项目兼容;但由于我们**未复制**其代码,当前无需附带其许可证正文。
|
|
72
|
+
- 引用论文结论属学术引用,不构成代码衍生。
|
|
73
|
+
- 若将来引入任何第三方代码,必须在此文件登记并同步 `LICENSE`/`NOTICE`。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 6. 自动守卫
|
|
78
|
+
|
|
79
|
+
`test/license-provenance.test.mjs` 会在每次 `npm test` 时校验:
|
|
80
|
+
|
|
81
|
+
- `src/**`、`worker/**`、`client.js` 的每个文件都以本项目 SPDX 头开始;
|
|
82
|
+
- 不存在第三方版权头或许可证正文(MIT/Apache 样板文本);
|
|
83
|
+
- 不存在 `*.rs`、`Cargo.toml`、`vendor/`、`third_party/` 等 vendored 痕迹;
|
|
84
|
+
- `package.json` 无运行时/peer/optional 依赖(无法把参考实现当依赖拉进来);
|
|
85
|
+
- 源码不 `import`/`require` `dsh-ops`、`harnessmm` 等第三方包。
|
|
86
|
+
|
|
87
|
+
守卫失败即视为违反本节约束。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/dsh-master",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"build": "npm run build:host && npm run build:worker"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@twinsearth/dsh-fang-contract": "^1.
|
|
78
|
+
"@twinsearth/dsh-fang-contract": "^1.1.0",
|
|
79
79
|
"@types/node": "^24.0.0",
|
|
80
80
|
"typescript": "^5.9.0"
|
|
81
81
|
},
|
package/src/contract/client.js
CHANGED
|
@@ -81,6 +81,13 @@ export function createContractClient(options = {}) {
|
|
|
81
81
|
async resolveDecision(params, invokeOptions = {}) { return await client.invoke(METHODS.DECISION_RESOLVE, params, invokeOptions) },
|
|
82
82
|
async listSessions(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.SESSION_LIST, params, invokeOptions) },
|
|
83
83
|
async orchestratorStatus(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.ORCHESTRATOR_STATUS, params, invokeOptions) },
|
|
84
|
+
/** contract 1.1:批量投递回流事件(按 event.id 幂等;返回回执与游标)。 */
|
|
85
|
+
async publishRelay(events, invokeOptions = {}) {
|
|
86
|
+
const params = Array.isArray(events) ? { events } : (events && typeof events === 'object' ? events : { events: [] })
|
|
87
|
+
return await client.invoke(METHODS.RELAY_PUBLISH, params, invokeOptions)
|
|
88
|
+
},
|
|
89
|
+
/** contract 1.1:读取对端回流状态(对端自报 peer/cursor/累计计数)。 */
|
|
90
|
+
async relayStatus(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.RELAY_STATUS, params, invokeOptions) },
|
|
84
91
|
}
|
|
85
92
|
return client
|
|
86
93
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export const CONTRACT_PACKAGE = '@twinsearth/dsh-fang-contract'
|
|
14
|
-
export const CONTRACT_VERSION = '1.
|
|
14
|
+
export const CONTRACT_VERSION = '1.1.0'
|
|
15
15
|
export const PROTOCOL = 'fang-master'
|
|
16
16
|
export const PROTOCOL_VERSION = 1
|
|
17
17
|
export const MIN_COMPATIBLE_MAJOR = 1
|
|
@@ -27,6 +27,8 @@ export const METHODS = Object.freeze({
|
|
|
27
27
|
DECISION_RESOLVE: 'fang.decision.resolve',
|
|
28
28
|
SESSION_LIST: 'fang.session.list',
|
|
29
29
|
ORCHESTRATOR_STATUS: 'fang.orchestrator.status',
|
|
30
|
+
RELAY_PUBLISH: 'fang.relay.publish',
|
|
31
|
+
RELAY_STATUS: 'fang.relay.status',
|
|
30
32
|
})
|
|
31
33
|
|
|
32
34
|
/** 方法 → 契约版本(用于协商后的方法裁剪)。 */
|
|
@@ -40,10 +42,12 @@ export const METHOD_VERSION = Object.freeze({
|
|
|
40
42
|
[METHODS.DECISION_RESOLVE]: '1.0',
|
|
41
43
|
[METHODS.SESSION_LIST]: '1.0',
|
|
42
44
|
[METHODS.ORCHESTRATOR_STATUS]: '1.0',
|
|
45
|
+
[METHODS.RELAY_PUBLISH]: '1.1',
|
|
46
|
+
[METHODS.RELAY_STATUS]: '1.1',
|
|
43
47
|
})
|
|
44
48
|
|
|
45
49
|
export const CAPABILITIES = Object.freeze([
|
|
46
|
-
'sync', 'async', 'idempotent', 'stream-events', 'file-outbox',
|
|
50
|
+
'sync', 'async', 'idempotent', 'stream-events', 'file-outbox', 'event-relay',
|
|
47
51
|
])
|
|
48
52
|
|
|
49
53
|
export const ROLES = Object.freeze(['mother', 'essence', 'fork'])
|
|
@@ -77,3 +81,14 @@ export const LEGACY_SERVICE_KEYS = Object.freeze({
|
|
|
77
81
|
sessionCatalog: 'fangSessionCatalog',
|
|
78
82
|
orchestrator: 'fang',
|
|
79
83
|
})
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 回流接口的硬上限(1.1)。超限是**协议错误**而不是静默截断:
|
|
87
|
+
* 调用方必须自己分批,服务端不得悄悄丢事件(回流要可对账)。
|
|
88
|
+
*/
|
|
89
|
+
export const RELAY_LIMITS = Object.freeze({
|
|
90
|
+
maxBatch: 128,
|
|
91
|
+
maxEventBytes: 32_000,
|
|
92
|
+
maxPayloadBytes: 512_000,
|
|
93
|
+
maxRecords: 5_000,
|
|
94
|
+
})
|
package/src/contract/index.js
CHANGED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 回流(relay)接口 —— contract 1.1 新增能力。
|
|
6
|
+
*
|
|
7
|
+
* 背景:房主(dsh-Master)的成本总线产生 `cost:event` / `cost:degraded` /
|
|
8
|
+
* `cost:substitution` 三类事件,需要**单向**投递给瓮城(dsh-Fang)做产物回流与
|
|
9
|
+
* 信息栏展示。1.0 的方法表只有请求-响应式的 fork/decision/session,缺一个批量、
|
|
10
|
+
* 可去重、有回执的事件入口,因此 1.1 追加两个方法:
|
|
11
|
+
*
|
|
12
|
+
* * `fang.relay.publish` —— 批量投递事件,按 `event.id` 幂等去重,返回回执与游标;
|
|
13
|
+
* * `fang.relay.status` —— 对端自报回流状态(自身标识 peerLabel、游标、累计计数)。
|
|
14
|
+
*
|
|
15
|
+
* 兼容规则(README §2):新增方法只升 MINOR;1.0 的对端在 `hello` 协商时不会
|
|
16
|
+
* 宣告这两个方法,方法集被自动裁剪,调用方据此判定“回流不可用”,**不得**把
|
|
17
|
+
* 裁剪当成失败重试。
|
|
18
|
+
*
|
|
19
|
+
* 本文件只做协议与参考实现:不读写磁盘、不联网、不持有任何凭据。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { CONTRACT_VERSION, METHODS, RELAY_LIMITS } from './constants.js'
|
|
23
|
+
import { createContractServer } from './server.js'
|
|
24
|
+
|
|
25
|
+
function nowMs(now) {
|
|
26
|
+
const value = typeof now === 'function' ? now() : Date.now()
|
|
27
|
+
const n = Number(value)
|
|
28
|
+
return Number.isFinite(n) ? n : Date.now()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function asLimit(value, fallback) {
|
|
32
|
+
const n = Number(value)
|
|
33
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function byteLength(value) {
|
|
37
|
+
try { return Buffer.byteLength(JSON.stringify(value ?? null), 'utf8') } catch { return Number.POSITIVE_INFINITY }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 校验一批回流事件。逐条判定:合法的接受,非法的计 `rejected` 并给原因,
|
|
42
|
+
* 不因单条坏数据丢掉整批(但整批不可用会抛 `relay-events-invalid`)。
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown} events
|
|
45
|
+
* @param {{maxBatch?:number, maxEventBytes?:number, maxPayloadBytes?:number}} [limits]
|
|
46
|
+
*/
|
|
47
|
+
export function validateRelayEvents(events, limits = {}) {
|
|
48
|
+
const maxBatch = asLimit(limits.maxBatch, RELAY_LIMITS.maxBatch)
|
|
49
|
+
const maxEventBytes = asLimit(limits.maxEventBytes, RELAY_LIMITS.maxEventBytes)
|
|
50
|
+
const maxPayloadBytes = asLimit(limits.maxPayloadBytes, RELAY_LIMITS.maxPayloadBytes)
|
|
51
|
+
const errors = []
|
|
52
|
+
if (!Array.isArray(events)) {
|
|
53
|
+
return { ok: false, code: 'relay-events-invalid', errors: ['events-must-be-array'], accepted: [], rejected: [] }
|
|
54
|
+
}
|
|
55
|
+
if (events.length === 0) {
|
|
56
|
+
return { ok: false, code: 'relay-events-invalid', errors: ['events-empty'], accepted: [], rejected: [] }
|
|
57
|
+
}
|
|
58
|
+
if (events.length > maxBatch) {
|
|
59
|
+
return { ok: false, code: 'relay-events-invalid', errors: [`events-too-many(>${maxBatch})`], accepted: [], rejected: [] }
|
|
60
|
+
}
|
|
61
|
+
if (byteLength(events) > maxPayloadBytes) {
|
|
62
|
+
return { ok: false, code: 'relay-events-invalid', errors: [`payload-too-large(>${maxPayloadBytes})`], accepted: [], rejected: [] }
|
|
63
|
+
}
|
|
64
|
+
const accepted = []
|
|
65
|
+
const rejected = []
|
|
66
|
+
const seen = new Set()
|
|
67
|
+
for (const raw of events) {
|
|
68
|
+
const item = raw && typeof raw === 'object' ? raw : null
|
|
69
|
+
if (!item) { rejected.push({ id: null, reason: 'event-not-object' }); continue }
|
|
70
|
+
const id = typeof item.id === 'string' ? item.id.trim() : ''
|
|
71
|
+
if (!id) { rejected.push({ id: null, reason: 'id-required' }); continue }
|
|
72
|
+
if (typeof item.type !== 'string' || item.type.trim() === '') { rejected.push({ id, reason: 'type-required' }); continue }
|
|
73
|
+
if (byteLength(item) > maxEventBytes) { rejected.push({ id, reason: `event-too-large(>${maxEventBytes})` }); continue }
|
|
74
|
+
// 同一批内重复 id 也算重复投递:只接受第一次,其余计 duplicates。
|
|
75
|
+
if (seen.has(id)) { rejected.push({ id, reason: 'duplicate-in-batch' }); continue }
|
|
76
|
+
seen.add(id)
|
|
77
|
+
accepted.push({
|
|
78
|
+
id,
|
|
79
|
+
type: String(item.type),
|
|
80
|
+
at: Number.isFinite(Number(item.at)) ? Number(item.at) : 0,
|
|
81
|
+
payload: item.payload && typeof item.payload === 'object' ? item.payload : {},
|
|
82
|
+
meta: item.meta && typeof item.meta === 'object' ? item.meta : {},
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
return { ok: accepted.length > 0 || rejected.length === 0, errors, accepted, rejected }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 回流接收端(瓮城侧)的参考存储:按 event.id 幂等去重,保留有界回执环。
|
|
90
|
+
*
|
|
91
|
+
* @param {{
|
|
92
|
+
* peerLabel?: string, role?: string, instanceId?: string,
|
|
93
|
+
* maxRecords?: number, now?: () => number, limits?: object,
|
|
94
|
+
* }} [options]
|
|
95
|
+
*/
|
|
96
|
+
export function createRelayStore(options = {}) {
|
|
97
|
+
const peerLabel = String(options.peerLabel ?? 'contract-server')
|
|
98
|
+
const role = String(options.role ?? 'mother')
|
|
99
|
+
const instanceId = String(options.instanceId ?? '')
|
|
100
|
+
const maxRecords = asLimit(options.maxRecords, RELAY_LIMITS.maxRecords)
|
|
101
|
+
const limits = { ...RELAY_LIMITS, ...(options.limits && typeof options.limits === 'object' ? options.limits : {}) }
|
|
102
|
+
const now = typeof options.now === 'function' ? options.now : () => Date.now()
|
|
103
|
+
const seen = new Map()
|
|
104
|
+
const receipts = []
|
|
105
|
+
const counters = { accepted: 0, duplicates: 0, rejected: 0, batches: 0 }
|
|
106
|
+
let cursor = 0
|
|
107
|
+
let lastAt = 0
|
|
108
|
+
const lastErrors = []
|
|
109
|
+
|
|
110
|
+
function remember(receipt) {
|
|
111
|
+
receipts.push(receipt)
|
|
112
|
+
if (receipts.length > maxRecords) receipts.shift()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {{events?: unknown, batchId?: string, taskId?: string}} [params] */
|
|
116
|
+
function publish(params = {}) {
|
|
117
|
+
const validated = validateRelayEvents(params?.events, limits)
|
|
118
|
+
if (validated.ok === false) {
|
|
119
|
+
const error = new Error(validated.errors.join(', ') || 'relay-events-invalid')
|
|
120
|
+
error.code = 'relay-events-invalid'
|
|
121
|
+
error.retryable = false
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
counters.batches += 1
|
|
125
|
+
const acceptedIds = []
|
|
126
|
+
let duplicates = 0
|
|
127
|
+
for (const event of validated.accepted) {
|
|
128
|
+
if (seen.has(event.id)) { duplicates += 1; continue }
|
|
129
|
+
seen.set(event.id, { at: nowMs(now), cursor: cursor + 1 })
|
|
130
|
+
cursor += 1
|
|
131
|
+
counters.accepted += 1
|
|
132
|
+
acceptedIds.push(event.id)
|
|
133
|
+
remember({ id: event.id, type: event.type, at: event.at, cursor })
|
|
134
|
+
lastAt = nowMs(now)
|
|
135
|
+
}
|
|
136
|
+
counters.duplicates += duplicates
|
|
137
|
+
for (const item of validated.rejected) {
|
|
138
|
+
// 批内重复计入 duplicates;其余非法条目计入 rejected 并留原因(有界)。
|
|
139
|
+
if (item.reason === 'duplicate-in-batch') { duplicates += 1; counters.duplicates += 1; continue }
|
|
140
|
+
counters.rejected += 1
|
|
141
|
+
if (lastErrors.length < 50) lastErrors.push({ id: item.id, reason: item.reason })
|
|
142
|
+
}
|
|
143
|
+
const batchId = String(params?.batchId ?? `batch-${counters.batches}`)
|
|
144
|
+
const receipt = Object.freeze({
|
|
145
|
+
ok: true,
|
|
146
|
+
batchId,
|
|
147
|
+
taskId: params?.taskId === undefined ? null : String(params.taskId),
|
|
148
|
+
accepted: acceptedIds.length,
|
|
149
|
+
duplicates,
|
|
150
|
+
rejected: validated.rejected.length - duplicates,
|
|
151
|
+
cursor,
|
|
152
|
+
acceptedIds,
|
|
153
|
+
peer: peerLabel,
|
|
154
|
+
at: nowMs(now),
|
|
155
|
+
})
|
|
156
|
+
if (seen.size > maxRecords * 4) {
|
|
157
|
+
// 幂等窗口有界:按插入顺序淘汰最老的一半,避免长跑进程无界增长。
|
|
158
|
+
const overflow = seen.size - maxRecords * 2
|
|
159
|
+
let removed = 0
|
|
160
|
+
for (const key of seen.keys()) {
|
|
161
|
+
if (removed >= overflow) break
|
|
162
|
+
seen.delete(key)
|
|
163
|
+
removed += 1
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return receipt
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function status() {
|
|
170
|
+
return Object.freeze({
|
|
171
|
+
ok: true,
|
|
172
|
+
available: true,
|
|
173
|
+
peer: peerLabel,
|
|
174
|
+
role,
|
|
175
|
+
instanceId,
|
|
176
|
+
contractVersion: CONTRACT_VERSION,
|
|
177
|
+
cursor,
|
|
178
|
+
accepted: counters.accepted,
|
|
179
|
+
duplicates: counters.duplicates,
|
|
180
|
+
rejected: counters.rejected,
|
|
181
|
+
batches: counters.batches,
|
|
182
|
+
lastAt,
|
|
183
|
+
limits: { ...limits },
|
|
184
|
+
lastErrors: [...lastErrors],
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function describe() {
|
|
189
|
+
return Object.freeze({ peerLabel, records: receipts.length, seen: seen.size, ...counters, cursor })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return Object.freeze({ publish, status, describe, receipts: () => [...receipts] })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 把回流 store 包成契约 handler 表(挂到 `createContractServer({handlers})`)。
|
|
197
|
+
* @param {{publish:Function, status:Function}} store
|
|
198
|
+
*/
|
|
199
|
+
export function relayHandlers(store) {
|
|
200
|
+
if (!store || typeof store.publish !== 'function' || typeof store.status !== 'function') {
|
|
201
|
+
throw new Error('relayHandlers requires a relay store with publish/status')
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
[METHODS.RELAY_PUBLISH]: (params) => store.publish(params ?? {}),
|
|
205
|
+
[METHODS.RELAY_STATUS]: () => store.status(),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 便捷构造:带回流能力的契约服务端(瓮城侧挂载用)。
|
|
211
|
+
* `peerLabel` 是**自报**标识,房主侧据此记录“对端是谁”,不参与鉴权。
|
|
212
|
+
*/
|
|
213
|
+
export function createRelayContractServer(options = {}) {
|
|
214
|
+
const { peerLabel = 'contract-server', role = 'mother', instanceId = '', now, limits, maxRecords, extraHandlers = {}, ...rest } = options
|
|
215
|
+
const store = createRelayStore({ peerLabel, role, instanceId, now, limits, maxRecords })
|
|
216
|
+
const server = createContractServer({
|
|
217
|
+
role,
|
|
218
|
+
instanceId,
|
|
219
|
+
handlers: { ...relayHandlers(store), ...(extraHandlers && typeof extraHandlers === 'object' ? extraHandlers : {}) },
|
|
220
|
+
...rest,
|
|
221
|
+
})
|
|
222
|
+
return { server, store }
|
|
223
|
+
}
|