@bd7pil/reeve 0.1.4 → 0.1.5

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.
@@ -0,0 +1,129 @@
1
+ // 会话状态机(GATE 里唯一的一份回退状态)。
2
+ //
3
+ // 语义(复刻自 v1 时代那套 fallback 的行为规格):
4
+ // · **单向推进**:候选指针只增不减(attemptCount/fallbackIndex 语义);
5
+ // · **粘性**:切换成功后,同一 session 的后续请求从切换到的那个候选起(不回头);
6
+ // · **冷却**:失败的候选取记时间戳,冷却期(默认 60s)内不再被选;
7
+ // · **可选恢复**:`restore_primary_after_cooldown` 为真且所有失败记录的冷却都过期 → 指针归零;
8
+ // · **并发安全**:同一 session 的两次失败只推进一次(`advance` 带去重键);
9
+ // · **有界内存**:按 TTL 驱逐 + 条数上限。
10
+ //
11
+ // 时间是注入的(测试用假时钟,不 sleep)。
12
+ export function createSessionStore({ now = Date.now, policy = {}, maxSessions = 10_000 } = {}) {
13
+ // policy 允许传函数(配置热更后策略即时生效,状态不必重建)
14
+ const readPolicy = () => (typeof policy === 'function' ? policy() ?? {} : policy)
15
+ const ttlMs = () => Math.max(1, Number(readPolicy().session_ttl_seconds ?? 1800)) * 1000
16
+ const cooldownMs = () => Math.max(0, Number(readPolicy().cooldown_seconds ?? 60)) * 1000
17
+ /** @type {Map<string, {agent: string|null, touchedAt: number, chains: Map<string, {index: number, failed: Map<string, number>, lastNext: {key: string, model: string|null}|null}>}>} */
18
+ const sessions = new Map()
19
+
20
+ const record = (sessionID) => {
21
+ let rec = sessions.get(sessionID)
22
+ if (!rec) {
23
+ rec = { agent: null, touchedAt: now(), chains: new Map() }
24
+ sessions.set(sessionID, rec)
25
+ }
26
+ rec.touchedAt = now()
27
+ return rec
28
+ }
29
+ const chainRec = (sessionID, chainName) => {
30
+ const rec = record(sessionID)
31
+ let c = rec.chains.get(chainName)
32
+ if (!c) {
33
+ c = { index: 0, failed: new Map(), lastNext: null }
34
+ rec.chains.set(chainName, c)
35
+ }
36
+ return c
37
+ }
38
+ const sweep = () => {
39
+ const cutoff = now() - ttlMs()
40
+ for (const [id, rec] of sessions) if (rec.touchedAt < cutoff) sessions.delete(id)
41
+ if (sessions.size <= maxSessions) return
42
+ // 超上限:按 touchedAt 从旧到新驱逐
43
+ const ordered = [...sessions.entries()].sort((a, b) => a[1].touchedAt - b[1].touchedAt)
44
+ for (let i = 0; i < ordered.length - maxSessions; i++) sessions.delete(ordered[i][0])
45
+ }
46
+
47
+ const cooling = (rec, key) => {
48
+ const failedAt = rec.failed.get(key)
49
+ if (failedAt === undefined) return false
50
+ if (cooldownMs() === 0) return false
51
+ const alive = now() - failedAt < cooldownMs()
52
+ if (!alive) rec.failed.delete(key) // 过期即清(顺带把恢复判断简化)
53
+ return alive
54
+ }
55
+
56
+ return {
57
+ now,
58
+ cooldownMs,
59
+ setAgent(sessionID, agent) {
60
+ if (typeof sessionID !== 'string' || sessionID.length === 0) return
61
+ record(sessionID).agent = typeof agent === 'string' && agent.length > 0 ? agent : null
62
+ sweep()
63
+ },
64
+ agentOf(sessionID) {
65
+ const rec = sessions.get(sessionID)
66
+ if (!rec) return null
67
+ if (now() - rec.touchedAt >= ttlMs()) {
68
+ sessions.delete(sessionID)
69
+ return null
70
+ }
71
+ return rec.agent
72
+ },
73
+ /** 该 session 在这条链上的起点索引(粘性 + 冷却后恢复)。 */
74
+ startIndex(sessionID, chainName, keys) {
75
+ const rec = chainRec(sessionID, chainName)
76
+ if (rec.index > 0 && readPolicy().restore_primary_after_cooldown === true) {
77
+ const anyCooling = keys.some((k) => cooling(rec, k))
78
+ if (!anyCooling) rec.index = 0
79
+ }
80
+ return Math.min(rec.index, Math.max(0, keys.length - 1))
81
+ },
82
+ /** 某个候选此刻是不是在冷却里。 */
83
+ isCooling(sessionID, chainName, key) {
84
+ return cooling(chainRec(sessionID, chainName), key)
85
+ },
86
+ /** 失败:记时间戳(冷却起点)。**不**推进指针(推进由 noteSuccess/advance 决定)。 */
87
+ noteFailure(sessionID, chainName, key) {
88
+ const rec = chainRec(sessionID, chainName)
89
+ rec.failed.set(key, now())
90
+ sweep()
91
+ },
92
+ /** 成功:把指针钉在这个候选上(粘性)。 */
93
+ noteSuccess(sessionID, chainName, key, keys) {
94
+ const rec = chainRec(sessionID, chainName)
95
+ const at = keys.indexOf(key)
96
+ if (at >= 0) rec.index = at
97
+ rec.failed.delete(key)
98
+ sweep()
99
+ },
100
+ /**
101
+ * 插件侧要"下一个候选"时用(流已开始/沉默这两类层内换不了的情形)。
102
+ * `dedupeKey` 保证同一次失败被两条事件各触发一次时**只推进一次**。
103
+ * @returns {{ model: string|null, index: number }} model 为 null = 没有下一个了
104
+ */
105
+ advance(sessionID, chainName, keys, { dedupeKey = null } = {}) {
106
+ const rec = chainRec(sessionID, chainName)
107
+ if (dedupeKey !== null && rec.lastNext?.key === dedupeKey) return { model: rec.lastNext.model, index: rec.index }
108
+ const from = Math.max(rec.index, 0)
109
+ let nextIndex = -1
110
+ for (let i = from + 1; i < keys.length; i++) {
111
+ if (cooling(rec, keys[i])) continue
112
+ nextIndex = i
113
+ break
114
+ }
115
+ const model = nextIndex >= 0 ? keys[nextIndex] : null
116
+ if (nextIndex >= 0) rec.index = nextIndex
117
+ rec.lastNext = { key: dedupeKey, model }
118
+ sweep()
119
+ return { model, index: rec.index }
120
+ },
121
+ snapshot() {
122
+ return {
123
+ sessions: sessions.size,
124
+ cooling: [...sessions.values()].reduce((n, r) => n + [...r.chains.values()].reduce((m, c) => m + [...c.failed.keys()].filter((k) => cooling(c, k)).length, 0), 0),
125
+ }
126
+ },
127
+ size: () => sessions.size,
128
+ }
129
+ }
package/src/index.js CHANGED
@@ -33,15 +33,16 @@ import { Supervisor } from './supervisor.js'
33
33
  import { reconcileParent } from './reconcile.js'
34
34
  import { SnapshotWriter, buildSnapshot, projectAgentTable } from './snapshot.js'
35
35
  import { SUBAGENT_ROSTER, withRoster } from './roster.js'
36
- import { readSetupToken, startOwnedProxy } from './proxy.js'
37
- import { REEVE_TIERS, BUILTINS_TO_HIDE, mergeTierModels, pickDefaultAgent, readEndpoint } from './models.js'
38
- import { STATS_DEFAULTS, aggregateBifrostRows, aggregateLedger, buildStats, fetchBifrostLogs, fetchBifrostModels, mergeModelCandidates } from './stats.js'
36
+ import { probeGate, startGate } from './gate/gate.js'
37
+ import { defaultFallbackConfigPath } from './gate/config.js'
38
+ import { REEVE_TIERS, BUILTINS_TO_HIDE, mergeTierModels, pickDefaultAgent, readEndpoint, resolveGateEndpoint } from './models.js'
39
+ import { STATS_DEFAULTS, aggregateGateRows, aggregateLedger, buildStats, fetchGateModels, mergeModelCandidates, readLedgerTail } from './stats.js'
39
40
  import { loadAgents, loadSkills } from './register.js'
40
41
  import { ensureProviderBlock, reeveProviderBlock } from './config-write.js'
41
42
 
42
43
  const SUBAGENT_TOOL = 'subagent'
43
- /** 自持代理的默认端口(与写进配置的端点一致)。 */
44
- const PROXY_DEFAULT_PORT = 8080
44
+ /** GATE(唯一代理层)的默认端口(与写进配置的端点一致)。 */
45
+ const GATE_DEFAULT_PORT = 8080
45
46
 
46
47
  export default {
47
48
  id: PLUGIN_ID,
@@ -73,10 +74,8 @@ export default {
73
74
  // 名册注入的证据:注入成功过一次就记下来(进快照)——"藏起来不削弱委派"这条设计
74
75
  // 靠它可核对,而不是靠"模型应该看得见"。
75
76
  let roster = null
76
- /** 代理层自持的状态(进快照:面板可显示、上机可核对)。必须在快照块之前声明。 */
77
- let proxyState = null
78
- /** 链文件的状态(代理那边在维护,这里只留引用给快照)。必须在快照块之前声明。 */
79
- let chainRef = null
77
+ /** GATE 的运行态(进快照:面板可显示、上机可核对)。必须在快照块之前声明。 */
78
+ let gateState = null
80
79
  /** 性能统计(进快照)。必须在快照块之前声明。 */
81
80
  let statsState = null
82
81
  /** 模型候选(面板的链编辑选择器用)。必须在快照块之前声明。 */
@@ -130,12 +129,11 @@ export default {
130
129
  version: PLUGIN_VERSION,
131
130
  agents: agentTable,
132
131
  roster,
133
- proxy: proxyState,
134
- chain: chainRef,
135
132
  models: modelsState,
136
133
  registered: { agents: registeredAgents, skills: registeredSkills },
137
134
  endpoint: endpointState,
138
135
  stats: statsState,
136
+ gate: gateState,
139
137
  })
140
138
  snapshotFlush = () => snapshot.schedule(build)
141
139
  ledger.onChange = () => snapshot.schedule(build)
@@ -149,7 +147,7 @@ export default {
149
147
  // (只加不改 / 备份 / 写完自校验 / 保留注释)。
150
148
  let endpointWrite = null
151
149
  if (options.ownEndpoint !== false) {
152
- const wantPort = Number(options.proxy?.port ?? PROXY_DEFAULT_PORT)
150
+ const wantPort = resolveGateEndpoint({ gate: options.gate === true ? null : options.gate, proxy: options.proxy === true ? null : options.proxy, fallbackPort: GATE_DEFAULT_PORT }).port
153
151
  endpointWrite = await ensureProviderBlock({
154
152
  configDir: options.engineConfigDir ?? null,
155
153
  block: reeveProviderBlock({ port: wantPort }),
@@ -159,60 +157,72 @@ export default {
159
157
  endpointState = { status: endpointWrite.status, path: endpointWrite.path, detail: endpointWrite.detail }
160
158
  }
161
159
 
162
- // ---- 代理层自持(opt-in:options.proxy.enabled)----
163
- // 纯插件形态:取件 → 派生链 → 拉起/复用 → 等就绪 → 收敛 → 守护。
164
- // 不需要任何外部脚本;失败只记日志(代理起不来时引擎照常跑,模型会吵地报错)。
165
- let proxyHandle = null
166
- // 两种写法都行:`proxy: true`(简写)或 `proxy: { enabled: true, ... }`
167
- // 默认开:这样配置里可以真的只写一行插件(`proxy: false` 关闭;`proxy: true` 或对象都行)
168
- if (options.proxy !== false && options.proxy?.enabled !== false) {
169
- // 端口不用用户重复写:从配置里的 `providers.reeve.settings.baseURL` 推(读不到就用 options/默认)
170
- let opts = options.proxy === true || options.proxy === undefined ? {} : options.proxy
171
- if (opts.port == null) {
172
- const endpoint = await readEndpoint({})
173
- if (endpoint) {
174
- opts = { ...opts, port: endpoint.port, host: endpoint.host }
175
- logger.debug(`[reeve] 代理端口取自配置:${endpoint.host}:${endpoint.port}(来自 ${endpoint.from})`)
160
+ // ---- GATE(代理层 —— 唯一入口;opt-out:`gate: false`)----
161
+ // 引擎认的是 `providers.reeve` 那个 baseURL,GATE 就占那个端口;路由/回退/执行/台账都在
162
+ // 它里面(唯一代理层,见 docs/GATE.md)。失败只记日志:引擎照常跑,模型会吵地报错。
163
+ //
164
+ // **复用优先**:引擎是**按工作区**加载插件的 —— 同一个引擎里可能有多个插件实例(每个工作区一个)。
165
+ // 第一个实例把 GATE 拉到端点口上,后面的实例**用健康检查接上**(`external: true`),
166
+ // 而不是去抢同一个口(抢必失败 → 快照里会出现假的"GATE 未就绪",面板跟着误报)。
167
+ // 自己起的才自己关:接上别人的实例在卸载时**不动**那台 GATE。
168
+ let gateHandle = null
169
+ if (options.gate !== false && options.gate?.enabled !== false) {
170
+ const endpoint = await readEndpoint({})
171
+ const { port: gatePort, host: gateHost } = resolveGateEndpoint({
172
+ gate: options.gate === true ? null : options.gate,
173
+ proxy: options.proxy === true ? null : options.proxy,
174
+ endpoint,
175
+ fallbackPort: GATE_DEFAULT_PORT,
176
+ })
177
+ const configPath = options.gate?.configPath ?? options.fallbackConfigPath ?? defaultFallbackConfigPath()
178
+ const probe = await probeGate({ port: gatePort, host: gateHost })
179
+ if (probe.ok) {
180
+ gateState = { ...probe.snapshot, reused: true }
181
+ logger.debug(`[reeve] GATE 已在 :${gatePort} 上跑着(复用;链 ${(probe.snapshot?.chains ?? []).join('/')})`)
182
+ } else {
183
+ try {
184
+ gateHandle = await startGate({
185
+ port: gatePort,
186
+ host: gateHost,
187
+ configPath,
188
+ ledgerPath: options.gate?.ledgerPath ?? undefined,
189
+ logger,
190
+ })
191
+ gateState = gateHandle.snapshot()
192
+ logger.debug(`[reeve] GATE::${gateHandle.port}(链 ${gateState.chains.join('/')})`)
193
+ } catch (err) {
194
+ gateState = { error: String(err).slice(0, 200) }
195
+ logger.debug(`[reeve] GATE 起不来(忽略,引擎照常):${gateState.error}`)
176
196
  }
177
197
  }
178
- // 状态一变就刷新快照 —— 尤其"后台取件完成、代理起来了"那一刻(否则面板会一直显示未就绪)
179
- const onState = (st) => {
180
- proxyState = { ready: st.ready, detail: st.detail, port: st.port, host: st.host }
181
- // 链的状态对象由代理那边持续更新(热更、收敛复核都会写它)—— 这里只留引用,快照时再取
182
- if (st.chain) chainRef = st.chain
183
- snapshotFlush()
184
- }
185
- proxyHandle = await startOwnedProxy({ opts, logger, onState })
186
- onState(proxyHandle)
187
- logger.debug(`[reeve] 代理层自持:${proxyHandle.ready ? '就绪' : '未就绪'} —— ${proxyHandle.detail ?? '?'}`)
198
+ // 状态一变就刷快照 —— 尤其"起来了"或"起不来(如实报错)"这一刻。
199
+ // 少了这一次,消费方只能等下一次台账变化才看到 gate 段(起不来时会一直显示 null)。
200
+ snapshotFlush()
188
201
  }
189
202
 
190
203
  const admission = acquireAdmissionRuntime(ctx.location?.directory, { limits })
191
204
 
192
- // ---- ⑦ 性能统计(Bifrost 归因 + 台账聚合 → 快照)----
193
- // 为什么在插件里做而不是面板里做:面板跑在 openchamber 的 iframe 里,**没有网络能力**,
194
- // 只有宿主给的 readFile 与 writeFile。而 `/api/logs`、`/v1/models` 是本机回环的 HTTP ——
195
- // 只有引擎进程里的插件够得着。所以插件取数 + 聚合,面板只读快照。
205
+ // ---- ⑦ 性能统计(GATE 台账聚合 + 编排台账聚合 → 快照)----
206
+ // 为什么在插件里做而不是面板里做:面板跑在 openchamber 的 iframe 里,**没有网络/文件能力**,
207
+ // 只有宿主给的 readFile 与 writeFile。台账是引擎进程能读到的本地文件;模型目录是本机回环
208
+ // 的 HTTP(GATE 的 `/v1/models`)—— 只有插件够得着。所以插件取数 + 聚合,面板只读快照。
196
209
  const statsOpts = typeof options.stats === 'object' && options.stats !== null ? options.stats : {}
197
210
  const statsIntervalMs = Number(statsOpts.intervalMs ?? STATS_DEFAULTS.intervalMs)
198
211
  const statsLimit = Number(statsOpts.limit ?? STATS_DEFAULTS.limit)
199
212
  const statsWindowMs = Number(statsOpts.windowMs ?? STATS_DEFAULTS.windowMs)
200
- // 模型目录多久刷一次。**别跟统计同频**:`/v1/models` 是一次真去上游的探测,
201
- // Bifrost 会为每个 provider 记一行日志(实测一次探测 = 7 行)—— 一分钟一次会把
202
- // 用户自己的归因日志灌满探测噪声。模型目录本身变化很慢,半小时足够。
213
+ // 模型目录多久刷一次。**别跟统计同频**:`/v1/models` 会真去上游探测池(9router 那条),
214
+ // 一分钟一次没必要。模型目录本身变化很慢,半小时足够。
203
215
  const modelsIntervalMs = Number(statsOpts.modelsIntervalMs ?? STATS_DEFAULTS.modelsIntervalMs)
204
216
  let statsTimer = null
205
- let statsSignature = null
206
217
  let statsInFlight = false
207
218
  let lastModelsAt = 0
208
219
 
209
220
  /** 链上已经在用的模型(面板选择器的候选之一:发现不全时至少这些能选)。 */
210
221
  const chainedRefs = () => {
211
222
  const out = []
212
- for (const spec of Object.values(chainRef?.tiers ?? {})) {
213
- for (const t of spec?.targets ?? []) if (t?.model) out.push(`${t.provider}/${t.model}`)
214
- for (const f of spec?.fallbacks ?? []) {
215
- const s = String(f ?? '')
223
+ for (const targets of Object.values(gateState?.chain_targets ?? {})) {
224
+ for (const ref of targets ?? []) {
225
+ const s = String(ref ?? '')
216
226
  if (s && !s.endsWith('/') && s.includes('/')) out.push(s)
217
227
  }
218
228
  }
@@ -221,53 +231,35 @@ export default {
221
231
  const engineModels = () => REEVE_TIERS.map((t) => `reeve/${t.id}`)
222
232
 
223
233
  const refreshStats = async () => {
224
- if (!proxyHandle || statsInFlight) return
234
+ if (!gateHandle || statsInFlight) return
225
235
  statsInFlight = true
226
236
  const now = Date.now()
227
- const { port, host, paths } = proxyHandle
237
+ const nowMs = now
228
238
  try {
229
- const token = await readSetupToken(paths.token)
230
- if (!token) {
231
- statsState = buildStats({ now, windowMs: statsWindowMs, source: 'unavailable', error: '读不到代理的 setup token(统计接口要它)', checkedAt: now })
232
- snapshotFlush()
233
- return
234
- }
235
- // 先探一行:`total_count` + 最新 id 没变就说明没有新调用 —— 省掉一次上百行的抓取
236
- const probe = await fetchBifrostLogs({ port, host, token, limit: 1 })
237
- if (probe.error) {
238
- statsState = buildStats({ now, windowMs: statsWindowMs, source: 'unavailable', error: `读 /api/logs 失败:${probe.error}`, checkedAt: now })
239
- snapshotFlush()
240
- return
241
- }
242
- if (probe.signature && probe.signature === statsSignature && statsState?.bifrost) {
243
- statsState = { ...statsState, checkedAt: now }
244
- snapshotFlush()
245
- return
246
- }
247
- const full = await fetchBifrostLogs({ port, host, token, limit: statsLimit })
248
- statsSignature = full.signature
249
- const bifrost = full.error ? null : aggregateBifrostRows(full.rows, { now, windowMs: statsWindowMs })
250
- const ledgerAgg = aggregateLedger(ledger.all(), { now })
239
+ const tail = await readLedgerTail({ path: gateHandle.ledgerPath, limit: statsLimit })
240
+ const gateAgg = tail.error ? null : aggregateGateRows(tail.rows, { now: nowMs, windowMs: statsWindowMs })
241
+ const ledgerAgg = aggregateLedger(ledger.all(), { now: nowMs })
251
242
  statsState = buildStats({
252
- now,
253
- checkedAt: now,
243
+ now: nowMs,
244
+ checkedAt: nowMs,
254
245
  windowMs: statsWindowMs,
255
- source: bifrost ? 'bifrost.api' : 'unavailable',
256
- error: full.error ? `读 /api/logs 失败:${full.error}` : null,
257
- bifrost,
246
+ source: gateAgg ? 'gate.ledger' : 'unavailable',
247
+ error: tail.error ? `读台账失败:${tail.error}` : null,
248
+ gate: gateAgg,
258
249
  orchestrate: {
259
250
  ...ledgerAgg,
260
251
  queue: { active: admission.activeCount, waiting: admission.waitingCount },
261
252
  },
262
253
  })
254
+ gateState = gateHandle.snapshot()
263
255
  // 顺手把模型候选也刷一遍(半小时一次;链编辑器的选择器用它)
264
- if (now - lastModelsAt >= modelsIntervalMs) {
265
- const discovered = await fetchBifrostModels({ port, host })
266
- lastModelsAt = now
256
+ if (nowMs - lastModelsAt >= modelsIntervalMs) {
257
+ const discovered = await fetchGateModels({ port: gateHandle.port, host: gateHandle.host })
258
+ lastModelsAt = nowMs
267
259
  const candidates = mergeModelCandidates({ discovered: discovered.models, chained: chainedRefs(), engine: engineModels() })
268
260
  modelsState = {
269
- generatedAt: now,
270
- source: discovered.error ? 'chain+engine' : 'bifrost',
261
+ generatedAt: nowMs,
262
+ source: discovered.error ? 'chain+engine' : 'gate',
271
263
  error: discovered.error,
272
264
  discovered: discovered.models.length,
273
265
  candidates,
@@ -281,11 +273,11 @@ export default {
281
273
  }
282
274
  }
283
275
 
284
- if (statsIntervalMs > 0 && proxyHandle) {
276
+ if (statsIntervalMs > 0 && gateHandle) {
285
277
  void refreshStats()
286
278
  statsTimer = setInterval(() => void refreshStats(), statsIntervalMs)
287
279
  statsTimer.unref?.()
288
- logger.debug(`[reeve] 性能统计:每 ${Math.round(statsIntervalMs / 1000)}s 刷一次(近 ${statsLimit} 行 / ${Math.round(statsWindowMs / 3600000)}h 窗口)`)
280
+ logger.debug(`[reeve] 性能统计:每 ${Math.round(statsIntervalMs / 1000)}s 刷一次(台账尾 ${statsLimit} 行 / ${Math.round(statsWindowMs / 3600000)}h 窗口)`)
289
281
  }
290
282
  const supervisor = new Supervisor({
291
283
  ledger,
@@ -571,7 +563,7 @@ export default {
571
563
  snapshot.cancel()
572
564
  void snapshot.flush().catch(() => {})
573
565
  }
574
- if (proxyHandle) void proxyHandle.dispose().catch(() => {})
566
+ if (gateHandle) void gateHandle.dispose().catch(() => {})
575
567
  logger.debug('[reeve] 已卸载')
576
568
  }
577
569
  },
package/src/models.js CHANGED
@@ -112,3 +112,14 @@ export function pickDefaultAgent(editor, id = 'foreman') {
112
112
  }
113
113
  return false
114
114
  }
115
+
116
+ /**
117
+ * GATE 该在哪个 `host:port` 上服务(纯函数,四级回落:显式 gate → 旧 proxy 键 → 配置里的端点 → 默认)。
118
+ * 抽出来是为了**可测**:真实回落链要碰机器上的真实配置(和真实的 18080),不该在单测里跑。
119
+ */
120
+ export function resolveGateEndpoint({ gate = null, proxy = null, endpoint = null, fallbackPort = 8080, fallbackHost = '127.0.0.1' } = {}) {
121
+ return {
122
+ port: Number(gate?.port ?? proxy?.port ?? endpoint?.port ?? fallbackPort),
123
+ host: gate?.host ?? endpoint?.host ?? fallbackHost,
124
+ }
125
+ }
package/src/snapshot.js CHANGED
@@ -92,7 +92,7 @@ export class SnapshotWriter {
92
92
  */
93
93
  export function buildSnapshot(
94
94
  records,
95
- { now = Date.now(), version = null, agents = null, roster = null, proxy = null, registered = null, endpoint = null, chain = null, models = null, stats = null } = {},
95
+ { now = Date.now(), version = null, agents = null, roster = null, registered = null, endpoint = null, models = null, stats = null, gate = null } = {},
96
96
  ) {
97
97
  const tasks = records
98
98
  .map((r) => ({
@@ -123,8 +123,7 @@ export function buildSnapshot(
123
123
  version,
124
124
  agentTable: agents,
125
125
  roster,
126
- proxy,
127
- chain: chain == null ? null : projectChain(chain),
126
+ gate,
128
127
  models,
129
128
  registered,
130
129
  endpoint,
@@ -134,27 +133,6 @@ export function buildSnapshot(
134
133
  }
135
134
  }
136
135
 
137
- /**
138
- * 链的状态 → 面板要的那一小块。
139
- * `tiers` 就是 `~/.reeve/chain.json` 里那份形状(面板编辑的就是它,写回也是它)。
140
- */
141
- export function projectChain(chain) {
142
- if (!chain) return null
143
- return {
144
- tiersPath: chain.tiersPath ?? null,
145
- source: chain.source ?? null,
146
- from: chain.from ?? null,
147
- derived: chain.derived === true,
148
- tiers: chain.tiers ?? null,
149
- problems: chain.problems ?? [],
150
- error: chain.error ?? null,
151
- changedAt: chain.changedAt ?? null,
152
- appliedAt: chain.appliedAt ?? null,
153
- drift: chain.drift ?? null,
154
- lastError: chain.lastError ?? null,
155
- }
156
- }
157
-
158
136
  /**
159
137
  * 从 `ctx.agent.list()` 的返回里抽出"谁可见"的最小形状。
160
138
  * 读不到就返回 null —— 快照里没有这一节,不假装知道。