@bd7pil/reeve 0.1.5 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bd7pil/reeve",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "REEVE 编排层(opencode v2 插件):agent/skill 注册 + 子任务台账 + GATE 代理层(路由/回退/台账)+ 性能统计",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/provision.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "name": "reeve",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
5
5
  "license": "MIT",
6
6
  "//": "REEVE 在 ocrc 实例里的装配说明。**没有二进制工件** —— 代理层是插件内的 GATE(纯 JS),不需要任何本地服务(Bifrost 时代已结束)。",
7
7
  "host": {
@@ -15,7 +15,7 @@
15
15
  ]
16
16
  },
17
17
  "enginePlugin": {
18
- "spec": "@bd7pil/reeve@0.1.5",
18
+ "spec": "@bd7pil/reeve@0.1.6",
19
19
  "kind": "package",
20
20
  "entry": "~/.ocrc/components/reeve/node_modules/@bd7pil/reeve",
21
21
  "requiresPeer": []
package/src/constants.js CHANGED
@@ -1,10 +1,24 @@
1
1
  // REEVE 记账层的常量与状态机定义。
2
2
  // 施工图:docs/research/opencode-v2-plugin-api.md
3
3
 
4
+ import { readFileSync } from 'node:fs'
5
+
4
6
  export const PLUGIN_ID = 'reeve'
5
7
 
6
- /** 与 package.json 的 version 保持一致;快照文件里会带上,便于面板判断版本。 */
7
- export const PLUGIN_VERSION = '0.1.4'
8
+ /**
9
+ * 插件版本(快照文件里会带上,便于面板/台账判断"跑的是哪一版")。
10
+ *
11
+ * **从 package.json 读**,不再手抄一个常量:手抄的那种"保持一致"是句祝愿 ——
12
+ * 0.1.5 发布时就漂过(package.json 0.1.5、常量还写 0.1.4,两端快照都报了旧版本)。
13
+ * 读不到就退回 '0.0.0'(只影响一个字符串,别让插件起不来)。
14
+ */
15
+ export const PLUGIN_VERSION = (() => {
16
+ try {
17
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version ?? '0.0.0'
18
+ } catch {
19
+ return '0.0.0'
20
+ }
21
+ })()
8
22
 
9
23
  /** 工具注册的命名空间。注意:真正决定"模型能否看见"的是 codemode:false(施工图 §3.1)。 */
10
24
  export const TOOL_NAMESPACE = 'reeve'
package/src/gate/gate.js CHANGED
@@ -90,8 +90,9 @@ const sendJson = (res, status, obj) => {
90
90
  }
91
91
 
92
92
  /**
93
- * 拉起 GATE。返回 handle:`{ port, dispose, snapshot, reload, sessions, ledgerPath, configPath }`。
94
- * 任何"起不来"的失败都**抛**(它是引擎启动路径上的东西,吵比静默好);运行期失败只记日志。
93
+ * 拉起 GATE。返回 handle:`{ port, detach, dispose, snapshot, reload, sessions, ledgerPath, configPath }`。
94
+ * 生命周期:`detach()` = 插件实例卸载但 GATE 继续服务(引擎级,活到进程退出);`dispose()` = 真关
95
+ * (测试与显式停止用)。任何"起不来"的失败都**抛**(它是引擎启动路径上的东西,吵比静默好);运行期失败只记日志。
95
96
  */
96
97
  export async function startGate({
97
98
  port,
@@ -350,6 +351,9 @@ export async function startGate({
350
351
  resolve()
351
352
  })
352
353
  })
354
+ // 引擎级服务:监听口**不**替引擎续命。引擎是长活进程,自己有事吊着;而 CLI 那种一次性
355
+ // 进程卸载插件后必须能立刻退出 —— 少了这一行,卸载后还挂着一个 ref 着的监听 handle。
356
+ server.unref?.()
353
357
 
354
358
  const watch = watchGateConfig({
355
359
  path: configPath,
@@ -371,6 +375,7 @@ export async function startGate({
371
375
  })
372
376
 
373
377
  const disposeFns = [() => watch.stop(), () => ledger.flush(), () => new Promise((r) => server.close(() => r()))]
378
+ let sweep = null
374
379
  return {
375
380
  port: server.address()?.port ?? port,
376
381
  host,
@@ -398,7 +403,37 @@ export async function startGate({
398
403
  problems,
399
404
  sessions: sessions.snapshot(),
400
405
  }),
406
+ /**
407
+ * 插件实例卸载时调用(引擎按工作区加载插件:工作区被闲置驱逐 = 这个实例卸载,
408
+ * **不是**引擎退出)。监听口与配置热更留着 —— 否则"起 GATE 的那个工作区"一被驱逐,
409
+ * 其它还活跃的工作区就永久拿不到代理(拆 0.1.7 前的实测)。这里只做两件事:
410
+ * ① 把已经空闲的 keep-alive 连接清掉;② 起一个 unref 的低频清扫,兜住"卸载后才变空闲"的那些
411
+ * (每条连接的生命周期里只有很短一段是 busy,第一条命令常赶不上);
412
+ * 定时器全是 unref 的,且 2s 后自停 —— 既不拖进程退出,也不长期干扰连接复用。
413
+ */
414
+ detach() {
415
+ const closeIdle = () => {
416
+ try {
417
+ server.closeIdleConnections?.()
418
+ } catch {
419
+ /* 尽力而为 */
420
+ }
421
+ }
422
+ closeIdle()
423
+ if (sweep) return
424
+ sweep = setInterval(closeIdle, 250)
425
+ sweep.unref?.()
426
+ const stop = setTimeout(() => {
427
+ if (sweep) clearInterval(sweep)
428
+ sweep = null
429
+ }, 2000)
430
+ stop.unref?.()
431
+ },
401
432
  async dispose() {
433
+ if (sweep) {
434
+ clearInterval(sweep)
435
+ sweep = null
436
+ }
402
437
  for (const f of disposeFns.reverse()) {
403
438
  try {
404
439
  await f()
package/src/index.js CHANGED
@@ -164,7 +164,9 @@ export default {
164
164
  // **复用优先**:引擎是**按工作区**加载插件的 —— 同一个引擎里可能有多个插件实例(每个工作区一个)。
165
165
  // 第一个实例把 GATE 拉到端点口上,后面的实例**用健康检查接上**(`external: true`),
166
166
  // 而不是去抢同一个口(抢必失败 → 快照里会出现假的"GATE 未就绪",面板跟着误报)。
167
- // 自己起的才自己关:接上别人的实例在卸载时**不动**那台 GATE。
167
+ // 卸载语义:**都不在卸载时关端口** —— GATE 是引擎级服务,插件实例(工作区)被闲置驱逐
168
+ // 并不意味着引擎退出。以前"自己起的自己关"会让起服务的工作区被驱逐后,其它活跃工作区的
169
+ // 请求永久拿不到代理(2026-10-22 两台机器实测)。`detach()` 只清空闲连接,不阻塞进程退出。
168
170
  let gateHandle = null
169
171
  if (options.gate !== false && options.gate?.enabled !== false) {
170
172
  const endpoint = await readEndpoint({})
@@ -563,7 +565,9 @@ export default {
563
565
  snapshot.cancel()
564
566
  void snapshot.flush().catch(() => {})
565
567
  }
566
- if (gateHandle) void gateHandle.dispose().catch(() => {})
568
+ // GATE 是引擎级服务:卸载只"松手"(清空闲连接、不关监听口)。真正关端口只在进程退出
569
+ // 或测试里显式 dispose;`detach` 的定时器都是 unref 的,CLI 一次性进程照样能立刻退出。
570
+ if (gateHandle) gateHandle.detach?.()
567
571
  logger.debug('[reeve] 已卸载')
568
572
  }
569
573
  },