@twinsearth/dsh-master 3.3.0 → 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 +2 -1
- package/client.js +1 -1
- 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/fang-relay.js +329 -0
- package/dist-host/cost/index.js +35 -2
- package/dist-host/cost/milestones.js +44 -6
- package/dist-host/kernel.js +1 -1
- package/dist-host/plugins/cost-optimizer.js +57 -5
- package/dist-host/plugins/superagent-router.js +18 -2
- package/docs/ARCHITECTURE.md +8 -5
- package/docs/CHANGELOG.md +16 -0
- package/docs/DESIGN-v3.3.0-cost-optimization.md +24 -19
- package/docs/EXPERIMENT-M5-fang-relay.md +117 -0
- package/docs/EXPERIMENT-M5-real-ab.md +6 -1
- 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/fang-relay.ts +318 -0
- package/src/cost/index.ts +34 -2
- package/src/cost/milestones.ts +41 -5
- package/src/kernel.ts +1 -1
- package/src/plugins/cost-optimizer.ts +48 -6
- package/src/plugins/superagent-router.ts +16 -2
package/dist-host/cost/index.js
CHANGED
|
@@ -15,14 +15,16 @@ import { createAuditChain } from "./audit.js";
|
|
|
15
15
|
import { createAttribution } from "./attribution.js";
|
|
16
16
|
import { createCostBus } from "./bus.js";
|
|
17
17
|
import { createHostSurfaceAdapter } from "./host-surface.js";
|
|
18
|
+
import { createFangRelay } from "./fang-relay.js";
|
|
18
19
|
import { buildSavingsReport, classifyEntry } from "./report.js";
|
|
19
20
|
import { buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP } from "./metrics.js";
|
|
20
21
|
import { MODULE_MANIFESTS, MODULE_IDS, validateManifests } from "./modules/manifests.js";
|
|
21
22
|
import { buildLedgerReport, formatLedgerReport, parseLedgerJsonl } from "./ledger-report.js";
|
|
22
23
|
import { buildToolFaceReport, planToolFace } from "./toolface.js";
|
|
23
24
|
import { buildReuseReport, formatReuseReport } from "./reuse-report.js";
|
|
24
|
-
export { createLedger, createBudget, createPolicy, createAuditChain, createAttribution, createCostBus, createHostSurfaceAdapter, buildSavingsReport, classifyEntry, buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP, MODULE_MANIFESTS, MODULE_IDS, validateManifests, buildLedgerReport, formatLedgerReport, parseLedgerJsonl, buildToolFaceReport, planToolFace, buildReuseReport, formatReuseReport };
|
|
25
|
+
export { createLedger, createBudget, createPolicy, createAuditChain, createAttribution, createCostBus, createHostSurfaceAdapter, createFangRelay, buildSavingsReport, classifyEntry, buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP, MODULE_MANIFESTS, MODULE_IDS, validateManifests, buildLedgerReport, formatLedgerReport, parseLedgerJsonl, buildToolFaceReport, planToolFace, buildReuseReport, formatReuseReport };
|
|
25
26
|
export * from "./types.js";
|
|
27
|
+
export { RELAY_PEERS, RELAY_DEFAULTS, createFangRelayFromContext, createRelayClient } from "./fang-relay.js";
|
|
26
28
|
/**
|
|
27
29
|
* @param {{
|
|
28
30
|
* mode?: string, allowlist?: string[] | {forks?:string[],tasks?:string[],tags?:string[],tools?:string[]},
|
|
@@ -30,6 +32,7 @@ export * from "./types.js";
|
|
|
30
32
|
* budgetLimits?: Array<{unit:string, limitTokens?:number, limitCost?:number, onExhaust?:string, degradeTo?:string}>,
|
|
31
33
|
* degradeModels?: string[],
|
|
32
34
|
* now?: () => number, maxEntries?: number, maxRecords?: number,
|
|
35
|
+
* relay?: object, relayClient?: object|null, relayOptions?: object, relayAutoAttach?: boolean,
|
|
33
36
|
* }} [options]
|
|
34
37
|
*/
|
|
35
38
|
export function createCostSystem(options = {}) {
|
|
@@ -46,6 +49,21 @@ export function createCostSystem(options = {}) {
|
|
|
46
49
|
});
|
|
47
50
|
const attribution = createAttribution({ ledger, now });
|
|
48
51
|
const bus = createCostBus({ now, maxEvents: options.maxBusEvents, relays: options.relays });
|
|
52
|
+
/**
|
|
53
|
+
* §5.6 / M5:瓮城回流出口。
|
|
54
|
+
* 传入 `relay`(已有适配器)则复用;否则用 `relayClient` 现造一个。
|
|
55
|
+
* 没有客户端时出口状态是 `relay-client-absent` —— 显式不可用,不是“探测中”。
|
|
56
|
+
*/
|
|
57
|
+
const relay = options.relay && typeof options.relay.status === 'function'
|
|
58
|
+
? options.relay
|
|
59
|
+
: createFangRelay({ client: options.relayClient ?? null, now, ...(options.relayOptions && typeof options.relayOptions === 'object' ? options.relayOptions : {}), logger: options.logger });
|
|
60
|
+
// 默认把出口挂到总线;测试可关掉自动挂载,手动投递。
|
|
61
|
+
if (options.relayAutoAttach !== false && typeof relay.attach === 'function') {
|
|
62
|
+
try {
|
|
63
|
+
relay.attach(bus);
|
|
64
|
+
}
|
|
65
|
+
catch { /* 回流挂载失败不影响记账 */ }
|
|
66
|
+
}
|
|
49
67
|
/** 宿主会话工具面适配器(可选;默认关闭,不依赖任何第三方插件)。 */
|
|
50
68
|
const hostSurface = createHostSurfaceAdapter({
|
|
51
69
|
ledger,
|
|
@@ -267,6 +285,19 @@ export function createCostSystem(options = {}) {
|
|
|
267
285
|
function subscribe(listener) {
|
|
268
286
|
return bus.subscribe(listener);
|
|
269
287
|
}
|
|
288
|
+
/** §5.6:探测回流对端(hello 协商 + relay 状态),结果并入 `status().relay`。 */
|
|
289
|
+
async function relayProbe(input = {}) {
|
|
290
|
+
return await relay.probe({ force: input.force === true });
|
|
291
|
+
}
|
|
292
|
+
/** §5.6:投递当前队列里的回流事件;返回回执摘要(不吞错,不谎报送达)。 */
|
|
293
|
+
async function relayFlush() {
|
|
294
|
+
if (typeof relay.flush !== 'function')
|
|
295
|
+
return { ok: false, code: 'relay-flush-unsupported' };
|
|
296
|
+
return await relay.flush();
|
|
297
|
+
}
|
|
298
|
+
function relayStatus() {
|
|
299
|
+
return relay.status();
|
|
300
|
+
}
|
|
270
301
|
function status() {
|
|
271
302
|
const ledgerSnapshot = ledger.snapshot();
|
|
272
303
|
const auditVerify = audit.verify();
|
|
@@ -290,6 +321,8 @@ export function createCostSystem(options = {}) {
|
|
|
290
321
|
unattributedProjectedTotal: attributionSnapshot.unattributedProjectedTotal,
|
|
291
322
|
},
|
|
292
323
|
bus: { count: busSnapshot.count, dropped: busSnapshot.dropped, subscribers: busSnapshot.subscribers, relays: busSnapshot.relays },
|
|
324
|
+
/** §5.6 / M5:瓮城回流出口的真实状态(可达则 available:true + 对端自报 peer)。 */
|
|
325
|
+
relay: relay.status(),
|
|
293
326
|
degraded: {
|
|
294
327
|
modules: [...degradedByModule.values()].map((item) => ({ ...item, phases: { ...item.phases } })),
|
|
295
328
|
occurrences: [...degradedByModule.values()].reduce((sum, item) => sum + item.occurrences, 0),
|
|
@@ -308,5 +341,5 @@ export function createCostSystem(options = {}) {
|
|
|
308
341
|
hostSurface: hostSurface.status(),
|
|
309
342
|
});
|
|
310
343
|
}
|
|
311
|
-
return Object.freeze({ ledger, budget, policy, audit, attribution, bus, hostSurface, recordUsage, authorize, attribute, attributeTotal, substitute, trace, degraded, query, replay, savingsReport, metrics, ledgerReport, subscribe, status });
|
|
344
|
+
return Object.freeze({ ledger, budget, policy, audit, attribution, bus, hostSurface, relay, recordUsage, authorize, attribute, attributeTotal, substitute, trace, degraded, query, replay, savingsReport, metrics, ledgerReport, subscribe, status, relayProbe, relayFlush, relayStatus });
|
|
312
345
|
}
|
|
@@ -24,6 +24,12 @@ export const MILESTONE_IDS = Object.freeze(['M1', 'M2', 'M3', 'M4', 'M5']);
|
|
|
24
24
|
export const M3_MIN_SAMPLES = 20;
|
|
25
25
|
/** M4 放行前要求的最小守卫准确率(与 ENFORCE_GATE.minAccuracy 一致)。 */
|
|
26
26
|
export const M3_MIN_ACCURACY = 0.98;
|
|
27
|
+
/**
|
|
28
|
+
* M5 回流放行条件:至少 3 条事件拿到对端回执,且对端自报身份在白名单内。
|
|
29
|
+
* 自环(同一进程里服务端+客户端)与未知 peer 一律不算达标。
|
|
30
|
+
*/
|
|
31
|
+
export const M5_MIN_RELAY_EVENTS = 3;
|
|
32
|
+
export const M5_RELAY_PEERS = Object.freeze(['dsh-fang-bundle', 'contract-server']);
|
|
27
33
|
export const MILESTONES = Object.freeze([
|
|
28
34
|
Object.freeze({
|
|
29
35
|
id: 'M1',
|
|
@@ -74,13 +80,16 @@ export const MILESTONES = Object.freeze([
|
|
|
74
80
|
* files?: string[], // 仓库里实际存在的文件(相对路径)
|
|
75
81
|
* guard?: {samples?: number, accuracy?: number}, // M3 的守卫准确率数据(来自 shadow 报告)
|
|
76
82
|
* forkExperiment?: {ok?: boolean}, // M4 的成功率对照(来自 gate 的 G5)
|
|
77
|
-
* fangRelay?: {available?: boolean
|
|
83
|
+
* fangRelay?: {available?: boolean, peer?: string, events?: number,
|
|
84
|
+
* sameProcess?: boolean, transport?: string, contractVersion?: string,
|
|
85
|
+
* reason?: string}, // M5 的回流对端证据(独立进程实测)
|
|
78
86
|
* }} evidence
|
|
79
87
|
*/
|
|
80
88
|
export function evaluateMilestones(evidence = {}) {
|
|
81
89
|
const files = new Set(Array.isArray(evidence.files) ? evidence.files : []);
|
|
82
90
|
const rows = [];
|
|
83
91
|
const done = new Set();
|
|
92
|
+
let relayEvidence = null;
|
|
84
93
|
for (const milestone of MILESTONES) {
|
|
85
94
|
const missingImplementation = milestone.implementation.filter((file) => !files.has(file));
|
|
86
95
|
const missingTests = milestone.tests.filter((file) => !files.has(file));
|
|
@@ -119,11 +128,34 @@ export function evaluateMilestones(evidence = {}) {
|
|
|
119
128
|
reasons.push('effect-needs-fork-success-comparison');
|
|
120
129
|
}
|
|
121
130
|
else if (milestone.id === 'M5') {
|
|
122
|
-
//
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
// 回流必须有**对端回执**才算达标:可用 + 非自环 + 对端自报身份在白名单 + 至少 3 条事件。
|
|
132
|
+
const relay = evidence?.fangRelay && typeof evidence.fangRelay === 'object' ? evidence.fangRelay : null;
|
|
133
|
+
const available = relay?.available === true;
|
|
134
|
+
const peer = String(relay?.peer ?? '');
|
|
135
|
+
const events = Math.max(0, Number(relay?.events ?? 0) || 0);
|
|
136
|
+
if (!available) {
|
|
137
|
+
reasons.push(`fang-relay-unavailable:${String(relay?.reason ?? 'not-provided')}`);
|
|
138
|
+
}
|
|
139
|
+
else if (relay?.sameProcess === true) {
|
|
140
|
+
reasons.push('fang-relay-self-loop');
|
|
141
|
+
}
|
|
142
|
+
else if (!M5_RELAY_PEERS.includes(peer)) {
|
|
143
|
+
reasons.push(`fang-relay-peer-invalid:${peer || 'missing'}`);
|
|
144
|
+
}
|
|
145
|
+
else if (events < M5_MIN_RELAY_EVENTS) {
|
|
146
|
+
reasons.push(`fang-relay-no-receipts(events=${events})`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
effect = 'verified';
|
|
150
|
+
}
|
|
151
|
+
relayEvidence = Object.freeze({
|
|
152
|
+
available,
|
|
153
|
+
peer: peer || null,
|
|
154
|
+
events,
|
|
155
|
+
sameProcess: relay?.sameProcess === true,
|
|
156
|
+
transport: relay?.transport ?? null,
|
|
157
|
+
contractVersion: relay?.contractVersion ?? relay?.peerContractVersion ?? null,
|
|
158
|
+
});
|
|
127
159
|
}
|
|
128
160
|
}
|
|
129
161
|
const status = !implemented
|
|
@@ -141,6 +173,12 @@ export function evaluateMilestones(evidence = {}) {
|
|
|
141
173
|
effect,
|
|
142
174
|
status,
|
|
143
175
|
reasons,
|
|
176
|
+
/** M5:回流对端证据(peer 自报 + 事件数 + 是否自环);其它阶段为 null。 */
|
|
177
|
+
relay: relayEvidence,
|
|
178
|
+
/** 剩余缺口说明(不隐藏)。 */
|
|
179
|
+
notes: milestone.id === 'M5' && relayEvidence?.peer === 'contract-server'
|
|
180
|
+
? ['peer=contract-server:契约对端独立进程实测,dsh-fang-bundle 采用 contract 1.1 后应变为 peer=dsh-fang-bundle']
|
|
181
|
+
: [],
|
|
144
182
|
}));
|
|
145
183
|
if (status === 'done')
|
|
146
184
|
done.add(milestone.id);
|
package/dist-host/kernel.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 与瓮城 dsh-Fang 的协作只走 DSH 服务键,不 import fang 代码。
|
|
8
8
|
*/
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
|
-
export const MASTER_VERSION = '3.3.
|
|
10
|
+
export const MASTER_VERSION = '3.3.1';
|
|
11
11
|
export const FANG_CONTRACT_MIN = '3.1.14';
|
|
12
12
|
export const SERVICE = Object.freeze({
|
|
13
13
|
master: 'dshMaster',
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 角色:仍只允许 fork(分身生产盘)。
|
|
17
17
|
*/
|
|
18
18
|
import { OPTIONAL_SERVICE, ROW, ROLE, SERVICE, detectRole, enabledOf, masterError, roleAllowed, safeGet, } from "../kernel.js";
|
|
19
|
-
import { createCostSystem, DEFAULT_BUDGET_LIMITS } from "../cost/index.js";
|
|
19
|
+
import { createCostSystem, createFangRelayFromContext, DEFAULT_BUDGET_LIMITS } from "../cost/index.js";
|
|
20
20
|
export const name = 'dsh-master-cost-optimizer';
|
|
21
21
|
export const inject = ['dshMaster'];
|
|
22
22
|
export const ROW_ID = ROW.cost;
|
|
@@ -37,6 +37,10 @@ function finiteNonNegative(value) {
|
|
|
37
37
|
* enabled?: boolean, mode?: string, targetSavingsRatio?: number,
|
|
38
38
|
* budget?: Array<{unit: string, limitTokens: number, onExhaust?: string}>,
|
|
39
39
|
* enforceAllowlist?: string[],
|
|
40
|
+
* relay?: {enabled?: boolean, httpBaseUrl?: string, httpToken?: string, fileDir?: string,
|
|
41
|
+
* timeoutMs?: number, maxBatch?: number, maxQueue?: number, probeTtlMs?: number,
|
|
42
|
+
* autoFlushMs?: number},
|
|
43
|
+
* dispatch?: {motherUrl?: string, motherToken?: string, contractFileDir?: string},
|
|
40
44
|
* }} [config]
|
|
41
45
|
*/
|
|
42
46
|
export function apply(ctx, config = {}) {
|
|
@@ -55,6 +59,12 @@ export function apply(ctx, config = {}) {
|
|
|
55
59
|
return { skipped: true, reason: 'master-core-unavailable' };
|
|
56
60
|
}
|
|
57
61
|
const targetSavingsRatio = finiteNonNegative(config.targetSavingsRatio);
|
|
62
|
+
/**
|
|
63
|
+
* §5.6 / M5:瓮城回流出口。
|
|
64
|
+
* 传输按「插件配置 relay → dispatch 配置 → 环境变量 DSH_FANG_*」装配;
|
|
65
|
+
* 一个都没有时不造客户端,出口状态显式 `relay-client-absent`。
|
|
66
|
+
*/
|
|
67
|
+
const relayWiring = createFangRelayFromContext(ctx, config, { logger: ctx?.logger });
|
|
58
68
|
const system = createCostSystem({
|
|
59
69
|
mode: config.mode,
|
|
60
70
|
allowlist: config.enforceAllowlist ?? config.allowlist,
|
|
@@ -65,8 +75,28 @@ export function apply(ctx, config = {}) {
|
|
|
65
75
|
hostSurface: config.hostSurface && typeof config.hostSurface === 'object'
|
|
66
76
|
? { enabled: config.hostSurface.enabled === true, source: config.hostSurface.source }
|
|
67
77
|
: undefined,
|
|
78
|
+
relay: relayWiring.relay,
|
|
79
|
+
logger: ctx?.logger,
|
|
68
80
|
});
|
|
69
81
|
const policy = system.policy.describe();
|
|
82
|
+
// M5:加载后异步探测一次回流对端(不阻塞 apply;没有客户端时立即返回 absent)。
|
|
83
|
+
try {
|
|
84
|
+
void Promise.resolve(system.relayProbe()).catch(() => { });
|
|
85
|
+
}
|
|
86
|
+
catch { /* 探测失败不影响主流程 */ }
|
|
87
|
+
/** M5:回流自动投递;没有客户端或 autoFlushMs=0 时不开定时器。 */
|
|
88
|
+
const autoFlushMs = Math.max(0, Number(config.relay?.autoFlushMs ?? 2_000) || 0);
|
|
89
|
+
let relayTimer = null;
|
|
90
|
+
if (relayWiring.client && autoFlushMs > 0) {
|
|
91
|
+
relayTimer = setInterval(() => { try {
|
|
92
|
+
void system.relayFlush().catch(() => { });
|
|
93
|
+
}
|
|
94
|
+
catch { /* ignore */ } }, autoFlushMs);
|
|
95
|
+
try {
|
|
96
|
+
relayTimer.unref?.();
|
|
97
|
+
}
|
|
98
|
+
catch { /* ignore */ }
|
|
99
|
+
}
|
|
70
100
|
// 3.2.x 兼容计数器(任务级实测汇总;系统层另有账本,两者口径不同、互不覆盖)。
|
|
71
101
|
const metrics = {
|
|
72
102
|
tasks: 0,
|
|
@@ -78,7 +108,7 @@ export function apply(ctx, config = {}) {
|
|
|
78
108
|
};
|
|
79
109
|
const service = {
|
|
80
110
|
id: SERVICE.cost,
|
|
81
|
-
version: '3.3.
|
|
111
|
+
version: '3.3.1',
|
|
82
112
|
role,
|
|
83
113
|
get mode() {
|
|
84
114
|
return system.policy.describe().mode;
|
|
@@ -307,12 +337,24 @@ export function apply(ctx, config = {}) {
|
|
|
307
337
|
substitutions(filter = {}) {
|
|
308
338
|
return system.audit.substitutions(filter);
|
|
309
339
|
},
|
|
340
|
+
/** M5:瓮城回流出口状态(只读;不可用时必须带具体原因)。 */
|
|
341
|
+
relayStatus() {
|
|
342
|
+
return system.relayStatus();
|
|
343
|
+
},
|
|
344
|
+
/** M5:立即探测回流对端(hello 协商 + relay 状态),返回最新状态。 */
|
|
345
|
+
async relayProbe(input = {}) {
|
|
346
|
+
return await system.relayProbe(input);
|
|
347
|
+
},
|
|
348
|
+
/** M5:立即投递队列中的回流事件;回执里的数字才是送达证据。 */
|
|
349
|
+
async relayFlush() {
|
|
350
|
+
return await system.relayFlush();
|
|
351
|
+
},
|
|
310
352
|
status() {
|
|
311
353
|
const current = system.policy.describe();
|
|
312
354
|
return {
|
|
313
355
|
ok: true,
|
|
314
356
|
id: 'cost-optimizer',
|
|
315
|
-
version: '3.3.
|
|
357
|
+
version: '3.3.1',
|
|
316
358
|
role,
|
|
317
359
|
mode: current.mode,
|
|
318
360
|
requestedMode: current.requestedMode,
|
|
@@ -332,7 +374,9 @@ export function apply(ctx, config = {}) {
|
|
|
332
374
|
attribution: system.attribution.snapshot(),
|
|
333
375
|
attributionSummary: system.status().attributionSummary,
|
|
334
376
|
bus: system.bus.snapshot(1),
|
|
335
|
-
/** §5.6
|
|
377
|
+
/** §5.6 / M5:瓮城回流出口(可达 ⇒ available:true + 对端自报 peer;否则带原因)。 */
|
|
378
|
+
relay: system.relayStatus(),
|
|
379
|
+
/** §5.6:内部事件类型(冒号命名)与对外出口状态。 */
|
|
336
380
|
events: { types: ['cost:event', 'cost:degraded', 'cost:substitution'], relays: system.bus.snapshot(1).relays },
|
|
337
381
|
degraded: system.status().degraded,
|
|
338
382
|
surfaces: system.status().surfaces,
|
|
@@ -352,7 +396,7 @@ export function apply(ctx, config = {}) {
|
|
|
352
396
|
doubleCountGuard: report.doubleCountGuard,
|
|
353
397
|
};
|
|
354
398
|
})(),
|
|
355
|
-
note: '3.3.
|
|
399
|
+
note: '3.3.1:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1 真实投递);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认)。',
|
|
356
400
|
};
|
|
357
401
|
},
|
|
358
402
|
};
|
|
@@ -360,6 +404,14 @@ export function apply(ctx, config = {}) {
|
|
|
360
404
|
ctx.effect(() => ctx.provide(SERVICE.cost, service));
|
|
361
405
|
}
|
|
362
406
|
catch { /* fake ctx */ }
|
|
407
|
+
try {
|
|
408
|
+
ctx.effect(() => () => {
|
|
409
|
+
if (relayTimer)
|
|
410
|
+
clearInterval(relayTimer);
|
|
411
|
+
relayTimer = null;
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch { /* fake ctx */ }
|
|
363
415
|
// 3.3.0 §11:可选子服务(主服务形态不变;消费方必须探测,不能假定存在)。
|
|
364
416
|
const optionalFaces = {
|
|
365
417
|
[OPTIONAL_SERVICE.costLedger]: Object.freeze({
|
|
@@ -310,6 +310,22 @@ export function apply(ctx, config = {}) {
|
|
|
310
310
|
}
|
|
311
311
|
})();
|
|
312
312
|
const tickets = id === null ? [...workerEvents.keys()] : [id];
|
|
313
|
+
/**
|
|
314
|
+
* §5.6 / M5:回流出口状态来自成本服务(3.3.1 起是真实探测,不再是硬编码)。
|
|
315
|
+
* 没有成本服务时如实给 `cost-service-unavailable`,不编造“未就绪”原因。
|
|
316
|
+
*/
|
|
317
|
+
const relayBlock = (() => {
|
|
318
|
+
try {
|
|
319
|
+
const cost = safeGet(ctx, SERVICE.cost);
|
|
320
|
+
if (cost && typeof cost.relayStatus === 'function') {
|
|
321
|
+
const relay = cost.relayStatus();
|
|
322
|
+
if (relay && typeof relay === 'object')
|
|
323
|
+
return relay;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch { /* 回流状态读取失败不阻塞成本摘要 */ }
|
|
327
|
+
return { target: 'fang', available: false, reason: 'cost-service-unavailable' };
|
|
328
|
+
})();
|
|
313
329
|
const rows = [];
|
|
314
330
|
const totals = { events: 0, degraded: 0, substitutions: 0, estimated: 0, savedTokens: 0 };
|
|
315
331
|
for (const key of tickets) {
|
|
@@ -346,8 +362,8 @@ export function apply(ctx, config = {}) {
|
|
|
346
362
|
totals,
|
|
347
363
|
/* §11:只读成本块(A 单价项 / B 跳过调用项分列 + 命中率),面板只读消费。 */
|
|
348
364
|
service: serviceBlock,
|
|
349
|
-
/* §5.6
|
|
350
|
-
relay:
|
|
365
|
+
/* §5.6 / M5:瓮城回流出口真实状态(不可用必须显式带原因)。 */
|
|
366
|
+
relay: relayBlock,
|
|
351
367
|
};
|
|
352
368
|
};
|
|
353
369
|
const service = {
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -113,15 +113,15 @@
|
|
|
113
113
|
- 安全审计 P0/P1 已加固(env 脱敏、进程树 kill、Mandate 一致性、secret 脱敏、token 估算);
|
|
114
114
|
- 不安装、不发布;等审核。
|
|
115
115
|
|
|
116
|
-
## 7. 3.3.0
|
|
116
|
+
## 7. 3.3.0 / 3.3.1 成本优化(已实施并发布)
|
|
117
117
|
|
|
118
118
|
```text
|
|
119
119
|
master-cost-optimizer(行,仅 fork)= 编排器 + 服务 dshMasterCost
|
|
120
120
|
│
|
|
121
121
|
系统层(常驻):ledger 账本 │ budget 预算闸 │ policy 灰度 │ attribution 归因 │ audit 审计链 │ bus 事件总线
|
|
122
|
-
│ hooks │ reports(→
|
|
123
|
-
模块层 A 工具面:A1 compact ✅ │ A2 disclosure
|
|
124
|
-
模块层 B 决策复用:B1 edg
|
|
122
|
+
│ hooks │ reports(→ 瓮城回流出口:fang-relay 真实投递)
|
|
123
|
+
模块层 A 工具面:A1 compact ✅ │ A2 disclosure ✅ │ A3 batch ✅
|
|
124
|
+
模块层 B 决策复用:B1 edg ✅ │ B2 gdr ✅ │ B3 ccs ✅ │ B4 rsi ✅
|
|
125
125
|
│ ToolModule SPI / DecisionModule SPI
|
|
126
126
|
Worker(fork)四切点:beforeModelCall → afterModelResponse → beforeToolDispatch → afterToolReceipt
|
|
127
127
|
```
|
|
@@ -132,4 +132,7 @@ master-cost-optimizer(行,仅 fork)= 编排器 + 服务 dshMasterCost
|
|
|
132
132
|
Agent Loop **不认识具体模块**;模块异常 fail-open 并留痕,因此新增模块无需改 `agent-loop.ts`。
|
|
133
133
|
- 四切点:`worker/cost-hooks.ts`,事件经 `sink.cost` 到宿主(HTTP sink 的 `cost` 事件 / Cordis `superagent:cost`)。
|
|
134
134
|
- A1 默认 **shadow**:计算「本可节省」但返回原文;只有 `enforce` 且紧凑结果更短才替换,且归因单独入账(不重复计消耗)。
|
|
135
|
-
-
|
|
135
|
+
- 瓮城回流(3.3.1):`src/cost/fang-relay.ts` 走 `@twinsearth/dsh-fang-contract` 1.1 的
|
|
136
|
+
`fang.relay.publish` / `fang.relay.status`;只有对端回执才算送达,不可用给具名原因。
|
|
137
|
+
- 详见 `docs/DESIGN-v3.3.0-cost-optimization.md`(§0 总体架构、§17 实施进度)、
|
|
138
|
+
`docs/EXPERIMENT-M5-real-ab.md`(真实 A/B)、`docs/EXPERIMENT-M5-fang-relay.md`(回流实测)。
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 3.3.1(2026-11-13 已发布)
|
|
2
|
+
|
|
3
|
+
> 三方发布:annotated tag `v3.3.1` + GitHub Release + npm `@twinsearth/dsh-master@3.3.1`。
|
|
4
|
+
> 同批发布接口包:`@twinsearth/dsh-fang-contract@1.1.0`(仓库 [TwinsEarth/dsh-fang-contract](https://github.com/TwinsEarth/dsh-fang-contract))。
|
|
5
|
+
> 门禁:G1–G6 全绿 + G7 已授权;里程碑 **M1–M5 全部 done**(M5 的 peer 见下)。
|
|
6
|
+
|
|
7
|
+
**主题:M5 收口 —— 瓮城回流接口从「硬编码 unavailable」变成「真实契约投递」。**
|
|
8
|
+
|
|
9
|
+
- **接口包升到 contract 1.1.0**(独立仓库):新增方法 `fang.relay.publish` / `fang.relay.status` 与能力 `event-relay`;新增 `src/relay.js`(`createRelayStore` / `validateRelayEvents` / `relayHandlers` / `createRelayContractServer`)——按 `event.id` 幂等、单调游标、有界回执、超限即协议错误(不静默截断);client 门面新增 `publishRelay()` / `relayStatus()`。方法版本裁剪规则不变:1.0 对端不会宣告 1.1 方法,`negotiate()` 自动裁剪。
|
|
10
|
+
- **传输加固上游化**:房主内嵌副本在 1.0.0 之后追加的逐调用 `timeoutMs`/`meta` 透传、`httpHeaders` 透传、HTTP 定时器 `unref`、file 回执/请求文件的有界重试清理(Windows EPERM)全部回灌进 1.1.0;`src/contract/` 与已发布包**逐字节相同**,由 `test/contract-conformance.test.mjs` 守卫(漂移即红)。
|
|
11
|
+
- **新增 `src/cost/fang-relay.ts`**:`createFangRelay()` 做三件事——`probe()`(hello 协商 → 方法裁剪判定 → 读 `fang.relay.status` 确认对端真在服务)、`enqueue()/flush()`(批量投递;回执才算送达;失败退回队列并把状态转不可用)、`status()`(同步给最近已知状态)。不可用原因全部具名:`relay-client-absent` / `hello-failed:*` / `peer-contract-without-relay:*` / `probe-threw:*` / `publish-failed:*`,不再是单个 `fang-channel-not-ready` 常量。
|
|
12
|
+
- **接线**:`createCostSystem()` 新增 `relay` / `relayClient` / `relayOptions` / `relayAutoAttach`,`status()` 增加 `relay`;`cost-optimizer` 从「插件配置 relay → dispatch 配置 → `DSH_FANG_*` 环境」装配客户端,服务面新增 `relayStatus()/relayProbe()/relayFlush()`,有客户端时按 `autoFlushMs`(默认 2s、`unref`)自动投递并在 `ctx.effect` 清理定时器;`superagent-router` 的 `cost(ticketId).relay` 改为读真实状态,没有成本服务时如实给 `cost-service-unavailable`。
|
|
13
|
+
- **M5 判定收紧**:`evaluateMilestones()` 的 M5 不再只看 `available` 布尔——要求 `peer ∈ {dsh-fang-bundle, contract-server}`、`events ≥ 3`(对端回执数)、`sameProcess !== true`(自环不算),并把 `peer` 与残余缺口写进结果行。
|
|
14
|
+
- **实测证据**:`scripts/fang-relay-probe.mjs` 起**独立进程**跑已发布的 contract 1.1.0 作对端(HTTP),本进程用内嵌副本作客户端,投递 6 条事件全部拿到回执(`accepted 6 / duplicates 0 / cursor 6 / failed 0 / dropped 0`,`sameProcess:false`,`transport:http`);证据与方法见 [`docs/EXPERIMENT-M5-fang-relay.md`](EXPERIMENT-M5-fang-relay.md)。M5 现状 `done`,`peer=contract-server`;瓮城 bundle 采用 contract 1.1 后应变为 `peer=dsh-fang-bundle`,该残余缺口写在结果行 `notes` 里,不隐藏。
|
|
15
|
+
- 测试:新增 `test/cost-fang-relay.test.mjs`(10 条:无客户端 / 1.0 对端 / hello 失败 / status 失败 / 真实投递 / 失败退回 / 重复回执 / 总线接线 / 装配 / 白名单);`test/contract-conformance.test.mjs` 改为逐字节一致性 + 双向 relay 互操作;`test/cost-milestones.test.mjs` 增加 peer / 自环 / 回执不足的反例;全量 **448 条:446 通过 + 2 条 Windows 并行跳过**。
|
|
16
|
+
|
|
1
17
|
## 3.3.0(2026-11-13 已发布)
|
|
2
18
|
|
|
3
19
|
> 三方发布:annotated tag 3.3.0 + GitHub Release + npm @twinsearth/dsh-master@3.3.0。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 房主 dsh-Master 3.3.0 架构设计:成本优化 & 省 Token 子插件(系统 + 模块)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 状态:**已实施并发布(3.3.0 / 3.3.1 三方发布:annotated tag + GitHub Release + npm)**。基线代码 `3.2.10`,本文件描述 3.3.0 的架构与落地路径;实施进度见 §17。
|
|
4
4
|
> 关联:`docs/ROADMAP.md`(3.3.0 = HarnessMM 经济性)、`docs/ARCHITECTURE.md`(角色矩阵 / `master-cost-optimizer` 行 → 仅 fork)、
|
|
5
5
|
> `src/plugins/cost-optimizer.ts`(3.2.x 的 observe 骨架,本设计在其上扩展且**不破坏**现有服务接口)。
|
|
6
6
|
> 参考实现:`dsh-ops`(MIT,工具层)、`HarnessMM`(Apache-2.0,arXiv:2610.32813,决策层)。**只移植协议语义、自研实现,不复制第三方代码。**
|
|
@@ -384,7 +384,7 @@ enforce —— 守卫命中才真正替换,且受白名单(forkId/task 标
|
|
|
384
384
|
|
|
385
385
|
- 内部:`cost:event`、`cost:degraded`、`cost:substitution`;
|
|
386
386
|
- 对外(3.3.0 可选):沿用 Worker→Router 的既有事件通道,新增 `cost` 事件类型上报 Router;Router 落库后供状态页与「分身信息栏」读取;
|
|
387
|
-
-
|
|
387
|
+
- 与瓮城「产物回流」对接:**3.3.1 起是真实投递**(contract 1.1 的 `fang.relay.*`),不可用时按具体原因显式 `available:false`,绝不假装成功。
|
|
388
388
|
|
|
389
389
|
**实施(`src/cost/bus.ts` + Router 侧聚合,已落地)**
|
|
390
390
|
|
|
@@ -395,8 +395,8 @@ enforce —— 守卫命中才真正替换,且受白名单(forkId/task 标
|
|
|
395
395
|
| Router 摘要 | 新增 `service.cost(ticketId?)`:按 ticket 汇总 `events / degraded / estimated / savedTokens / points / modules`,并给出 `relay` 状态;`status()` 新增 `cost` 与 `costRelay` |
|
|
396
396
|
| 只读路由 | `GET /dsh-master/api/superagent/cost[?ticketId=…]`(与状态路由同源,无写入口、不暴露 token) |
|
|
397
397
|
| 出口抽象 | 总线支持 `relays: [{name, publish, available, reason}]`;不可用出口只记状态不投递;出口抛异常**不影响主流程**但累加 `failed`(不静默吞) |
|
|
398
|
-
| 瓮城回流 | `
|
|
399
|
-
| 状态页/信息栏 | 数据已就绪(`status().cost` / `/cost`
|
|
398
|
+
| 瓮城回流 | **3.3.1 实现**(`src/cost/fang-relay.ts`):`probe()` 用契约 `hello` 协商判定对端是否宣告 `fang.relay.publish`(1.0 对端会被裁剪),再读 `fang.relay.status` 确认真的在服务;`enqueue()/flush()` 批量投递,**只有回执才算送达**(`accepted`/`duplicates`/`cursor`),失败退回队列并转 `publish-failed:*`。不可用原因具名:`relay-client-absent` / `hello-failed:*` / `peer-contract-without-relay:*` / `probe-threw:*` / `publish-failed:*` |
|
|
399
|
+
| 状态页/信息栏 | 数据已就绪(`status().cost` / `/cost` 路由,`relay` 读真实状态);UI 展示留后续版本 |
|
|
400
400
|
|
|
401
401
|
- 内部:`cost:event`、`cost:degraded`、`cost:substitution`;
|
|
402
402
|
- 对外(3.3.0 可选):沿用 Worker→Router 的既有事件通道,新增 `cost` 事件类型上报 Router;
|
|
@@ -902,8 +902,12 @@ node scripts/gate.mjs --input gate.json # 喂 G5 对照数据与 G6 证据
|
|
|
902
902
|
实测:`[rsi] 清单复核 通过:entries=1`。
|
|
903
903
|
- **面板**:`client.js` 的「房主」抽屉只读成本块(A/B 分列、governance、净节省、净亏任务、命中率),
|
|
904
904
|
数据来自 `/dsh-master/api/superagent/cost` 的 `service` 段;**无任何写操作**(有测试断言)。
|
|
905
|
-
-
|
|
906
|
-
`
|
|
905
|
+
- **与瓮城回流对接(3.3.1 收口)**:接口从「接口位」变成「真实投递」——
|
|
906
|
+
contract 1.1.0 新增 `fang.relay.publish` / `fang.relay.status`;房主侧 `src/cost/fang-relay.ts`
|
|
907
|
+
负责探测、批量投递、回执核对与失败退回;`scripts/fang-relay-probe.mjs` 起**独立进程**对端
|
|
908
|
+
跑已发布契约包,6 条事件全部拿到回执(`sameProcess:false`)。M5 判定随之收紧:
|
|
909
|
+
要求 `peer ∈ {dsh-fang-bundle, contract-server}`、`events ≥ 3`、非自环。
|
|
910
|
+
现状 `peer=contract-server`(瓮城 bundle 采用 1.1 后应变为 `dsh-fang-bundle`),该残余缺口写在结果行 `notes`。
|
|
907
911
|
- 测试:`test/cost-freeze-manifest.test.mjs`(7 条:摘要可复现、排序、复核通过、篡改检测、
|
|
908
912
|
输入漂移、阈值/可用性、空/异类清单拒收)。
|
|
909
913
|
|
|
@@ -997,24 +1001,24 @@ node scripts/milestones.mjs # 按仓库文件判定实现
|
|
|
997
1001
|
node scripts/milestones.mjs --input evidence.json # 追加 M3 守卫准确率 / M4 对照数据
|
|
998
1002
|
```
|
|
999
1003
|
|
|
1000
|
-
当前真实输出(2026-11-13
|
|
1004
|
+
当前真实输出(2026-11-13,3.3.1,运行 `node scripts/milestones.mjs --input <evidence>`):
|
|
1001
1005
|
|
|
1002
|
-
| 阶段 | 实现 | 依赖 | 效果 | 状态 |
|
|
1003
|
-
|
|
1004
|
-
| M1 | ✔ | ✔ | verified | **done** |
|
|
1005
|
-
| M2 | ✔ | ✔ |
|
|
1006
|
-
| M3 | ✔ | ✔ |
|
|
1007
|
-
| M4 | ✔ |
|
|
1008
|
-
| M5 | ✔ | ✔ |
|
|
1006
|
+
| 阶段 | 实现 | 依赖 | 效果 | 状态 | 证据 |
|
|
1007
|
+
|---|---|---|---|---|---|
|
|
1008
|
+
| M1 | ✔ | ✔ | verified | **done** | 记账 + 预算闸测试 |
|
|
1009
|
+
| M2 | ✔ | ✔ | verified | **done** | 真实 A/B 归因(tokens/task −37.6%) |
|
|
1010
|
+
| M3 | ✔ | ✔ | verified | **done** | shadow 样本 20 · 准确率 1.0 |
|
|
1011
|
+
| M4 | ✔ | ✔ | verified | **done** | 真实替换证书链 |
|
|
1012
|
+
| M5 | ✔ | ✔ | verified | **done** | 回流对端 `contract-server` · 6 条回执 |
|
|
1009
1013
|
|
|
1010
|
-
`实现 5/5 · 达标
|
|
1014
|
+
`实现 5/5 · 达标 5/5`
|
|
1011
1015
|
|
|
1012
1016
|
**四条判定规则(写死在代码里,有测试)**
|
|
1013
1017
|
|
|
1014
|
-
1. **实现 ≠ 达标**:M1 的效果由测试直接证明(记账 + 预算闸)⇒ `done`;M2–M5
|
|
1018
|
+
1. **实现 ≠ 达标**:M1 的效果由测试直接证明(记账 + 预算闸)⇒ `done`;M2–M5 需要各自的实测证据,缺证据只能是 `implemented-effect-unmeasured`。
|
|
1015
1019
|
2. **M4 必须等 M3 的数据**:M3 守卫准确率未达 `samples ≥ 20 且 accuracy ≥ 0.98` 时,M4 恒为 `blocked-by-dependency`(`dependency-unmet:M3`)—— 代码写完也不放行。
|
|
1016
1020
|
3. **M1 先行是结构性的**:抽掉 `src/cost/ledger.ts`,M2/M3/M4/M5 全部 `blocked-by-dependency`(有测试)。
|
|
1017
|
-
4.
|
|
1021
|
+
4. **瓮城回流必须有对端回执**:M5 的 `fangRelay` 要求 `available === true`、`peer` 在白名单内、`events ≥ 3`、`sameProcess !== true`;自环、未知 peer、回执不足一律 `unmeasured` 并点名原因(`fang-relay-self-loop` / `fang-relay-peer-invalid:*` / `fang-relay-no-receipts`)。
|
|
1018
1022
|
|
|
1019
1023
|
---
|
|
1020
1024
|
|
|
@@ -1048,6 +1052,7 @@ node scripts/milestones.mjs --input evidence.json # 追加 M3 守卫准确率 /
|
|
|
1048
1052
|
| **M1** 系统层 + 4 切点(observe) | ✅ 已实施(3.3.0-dev,未发布) | `src/cost/{ledger,budget,policy,audit,types,index}.ts`、`worker/cost-hooks.ts`、`cost-optimizer` 系统层接入、`test/cost-system.test.mjs`、`test/worker-cost-hooks.test.ts`;全量 197/197 |
|
|
1049
1053
|
| **M2** A1 紧凑输出 + shadow 归因 | ✅ 已实施(3.3.0-dev,未发布) | `worker/tool-compact.ts`(read/list/write/shell 格式化,off/shadow/enforce,默认 shadow)、`WorkerResult.cost.compaction`、`cost.recordAttribution()`(shadow 只计「本可节省」)、`test/worker-tool-compact.test.ts`、`test/cost-attribution.test.mjs` |
|
|
1050
1054
|
| **M2 收口** 系统层 attribution / bus + 模块 SPI | ✅ 已实施(3.3.0-dev,未发布) | `src/cost/attribution.ts`(按模块/任务聚合,enforce 才入账)、`src/cost/bus.ts`(事件总线 + 回流出口,订阅者异常隔离)、`worker/cost-spi.ts`(ToolModule / DecisionModule 契约)、`worker/cost-modules.ts`(注册表 + A1 适配,异常 fail-open)、`test/worker-cost-spi.test.ts`;Agent Loop 已改为只依赖注册表 |
|
|
1051
|
-
| **M2 余项 / M3–
|
|
1055
|
+
| **M2 余项 / M3–M4** | ✅ 已发布(3.3.0) | A2 渐进纰漏、A3 批量、B1–B4、真实 fork 对照实验、面板成本块(见 §14) |
|
|
1056
|
+
| **M5 回流收口** | ✅ 已发布(3.3.1) | contract 1.1.0(`fang.relay.publish`/`status`)+ `src/cost/fang-relay.ts` + `scripts/fang-relay-probe.mjs`;M5 由 `fang-relay-out-of-scope` 改为对端回执判定 |
|
|
1052
1057
|
|
|
1053
|
-
*
|
|
1058
|
+
*3.3.0 / 3.3.1 均已三方发布(annotated tag + GitHub Release + npm)。*
|
|
@@ -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 超时。
|
|
@@ -78,5 +78,10 @@ node scripts/fork-experiment.mjs --real --tasks 20 --out /tmp/real-ab.json --jso
|
|
|
78
78
|
M5 implemented-effect-unmeasured(原因:fang-relay-out-of-scope)
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
M5
|
|
81
|
+
M5 当时唯一未达标项是「与瓮城回流对接」—— 该通道当时没有协议实现,
|
|
82
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)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/dsh-master",
|
|
3
|
-
"version": "3.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
|
},
|