Skip to content

feat: dsh 进 PLUGIN_AGENTS——native 插件部署 + 拦截层(native 模式 D) - #525

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-09-04_dsh-native-plugin
Open

feat: dsh 进 PLUGIN_AGENTS——native 插件部署 + 拦截层(native 模式 D)#525
ranxianglei wants to merge 1 commit into
masterfrom
2026-09-04_dsh-native-plugin

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #521

What

dsh native plugin deployment + fetch interception layer — the task-D slice of #513's confirmed design (per-session dynamic port, thin bili daemon subcommand, in-process fetch interception). Shared primitives are structured for B/C reuse.

bili plugin install dsh (src/plugin-install.ts)

  • "dsh" joins PLUGIN_AGENTS; install/remove/status operate on every profile under $DSH_HOME/profiles/*, merging - insert: - name: <file://…/dist/agent/dsh-acp.js> into each profile's cordis.patch.yml (replaces bare [], appends otherwise).
  • Idempotent; stale entries dropped (parent - insert: group only when empty); .bili-bak backup per file once; clear error when no profiles exist yet.
  • Exported dshNativeInstalled() for the launcher.

Launcher compat (src/launcher.ts)

  • Legacy --patch overlay is skipped when a live native entry exists (any-profile rule → no double-registration of the same module via both layers). Fresh machines keep the legacy path untouched.
  • LaunchOptions += forceFresh / parentPid / instanceFile / autoUpdateOff.

Per-session proxy: bili daemon --fresh --json --parent-pid N (cli/server/config)

Fetch interception (src/agent/intercept.ts, new — shared primitive)

  • Wraps globalThis.fetch; rewrites ONLY requests whose origin == active upstream origin → <proxy>/bili/<url>; everything else passthrough; network failure on a rewritten call retries once direct (degrade, never break traffic); stats {rewritten, passthrough, degraded}; uninstall restores.
  • Targets resolve async per call through an 8s gate — this closes the first-fetch race found in e2e (agent's first LLM fetch fired before async bootstrap finished and went direct). Interceptor installs synchronously in apply(); regression test included.

dsh-acp (rewritten) + src/agent/dsh-settings.ts (new line-based reader)

  • Env BILLION_CONTEXT_PROXY wins (launcher attach mode), else spawn bili daemon. Upstream origin per issue spec: settings provider baseURL ?? llm-deepseek.baseURL ?? env DEEPSEEK_BASE_URL ?? official DeepSeek default (official route only).
  • Wire mode unchanged: no ACP tools, no conversation id, /acp still fetchStatusLatest (now reports the routing head). Under bili dsh the wrapped settings URL equals the proxy origin → interceptor passthrough → zero double-proxying by construction.

Triage notes

  • Verified before implementing: real dsh profile/patch composition order, proven - insert: entry shape from the existing launcher overlay, pi-ai's bare fetch() calls (no captured references → wrap works), parent-gone watcher semantics, instance-file APIs.
  • Layer judgment: the issue's "settings baseURL ?? env" chain was verified against the installed dsh source (deepseek-official route: config.baseURL ?? $DEEPSEEK_BASE_URL); the implementation extends it with the llm-deepseek section as a middle fallback so both route styles are covered.
  • No A/B PR exists yet in master or open PRs — this PR deliberately carries D plus the shared prerequisites (daemon subcommand, intercept module) that B/C will consume.

Pre-flight

  • typecheck clean · npm test: 1052 tests, 1051 pass, 1 fail = pre-existing env-dependent resolveClientCommand: codex/claude resolve to themselves (sandbox has a local codex binary; passes on runners without one; unrelated) · build OK (dist/agent/dsh-acp.js 12.56 KB, tsup entries unchanged)

E2E (real dsh, bare dsh --profile headless, no --patch)

  • Smoke round-trip through the intercepted proxy (exit 0); rewrite proven against a mock upstream (mock saw only unwrapped /v1/chat/completions; a non-SSE mock body made dsh render the proxy's own error string — proxy definitively in path).
  • Compression pipeline engaged on real dsh traffic: 32K route cap + ~35.6K-token prompt → preflight ran and fail-fast 502'd with correct adjudication (nothing left to fold — single-message prompts have no compressible history). Full multi-turn cycles remain covered by unit suite + e2e-codex (headless dsh is one-shot).
  • Reaping verified every run; real DSH_HOME install/remove roundtrip verified live then restored.
  • Limitations documented in devlog WORKLOG: /acp live rendering needs interactive TUI (unit-covered); sandbox argv limit caps headless prompt size.

- bili plugin install/remove/status dsh: cordis entry in every profile's
  cordis.patch.yml (idempotent, stale-entry drop, .bili-bak backups)
- launcher: skip legacy --patch overlay when a live native entry exists
- bili daemon --fresh --json --parent-pid N: per-session ephemeral-port
  proxy (BILI_INSTANCE_FILE handshake, no global instance/registry writes,
  auto-update off, parent-gone reap); config accepts port 0
- agent/intercept.ts: shared globalThis.fetch interceptor (origin-scoped
  /bili/ rewrite, async target gate, degrade-once direct retry)
- dsh-acp: sync install + async bootstrap (env attach or daemon spawn),
  upstream origin per settings baseURL ?? llm-deepseek ?? DEEPSEEK_BASE_URL
  ?? official default; /acp reports routing head; wire mode unchanged
- e2e: real dsh headless through intercepted proxy; compression pipeline
  engaged on real traffic (preflight fail-fast adjudication verified)
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

📦 Built Package Artifact

Branch: 2026-09-04_dsh-native-plugin (8755ede)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-525

Each push to this PR publishes a new version under the pr-525 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr525.tgz
npm install -g package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 Reviewing now. Plan: check out the PR head (8755ede, single commit over current master baf1ac5), run pre-flight (typecheck / test / build), then line-review the diff against the description — with focus on (a) both compression-mode semantics per AGENTS.md §6, (b) idempotency/stale-entry handling in plugin-install.ts, (c) the fetch-interceptor race gate, (d) the bili daemon port-0/session-instance-file path. Will reply with findings shortly.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 Review 结论:LGTM,可以合并(合并操作留给人工)。三个 minor 备注见下,均不阻塞。

验证了什么

Pre-flight(本地复跑,与描述完全一致)

  • npm run typecheck — clean
  • npm test — 1052 tests / 1051 pass / 1 fail = not ok 445 - resolveClientCommand: codex/claude resolve to themselves(tests/launcher.test.ts),环境依赖型失败(本沙箱装有本地 codex/claude binary),与改动无关 ✅
  • npm run build — OK,dist/agent/dsh-acp.js 12.56 KB,tsup entries 不变 ✅

逐条核对 PR 声称的组件,全部属实:

  • intercept.ts 原语:只重写 origin===upstream 的请求 → <proxy>/bili/<url>;network fail(TypeError、非 abort)降级直连一次;uninstall() 恢复前置 fetch;upstream === proxy 时禁用重写(管理调用不会自拦截)✅
  • 首 fetch race 修复正确且有序列保障apply() 里同步安装 wrapper(dsh-acp.tsapply() 顶部注释明确写了原因),per-call 异步目标解析 + 8s gate(超时→passthrough,setup 继续后台跑);回归测试存在(tests/agent-intercept.test.ts:128 "call issued before bootstrap finishes waits on the gate, then rewrites")✅
  • 健康探测依赖环已消除:代码里 settleRouting(...) 确实排在 fetch(base + "/__bili/health") 之前(WORKLOG bug Use kernel renderTags:"text-only" + forbid tag echoing in prompt #3 对应实现)✅
  • per-session proxy:port 0 → 子进程写 session instance file(BILI_INSTANCE_FILE)→ 父进程按 launchToken 轮询读回真实 origin/port;instance.ts 的三个 API 本来就接受可选 file 参数(不是新 API);port 0 时 stale-fallback probe 的 proxyOrigin(host, 0) 永不可能健康,无误 attach 路径 ✅
  • server.ts:session file 模式下跳过 registerInstanceAndWarn(instances.json),规避 无单实例守卫:多个 bili 代理进程并存共写 sessions 目录,伴随孤儿 ref、有注册表无 transcript、计数自相矛盾的污染簇 #394 双写者噪音;shutdown 清理两个文件 ✅
  • config.ts port 校验 1→0 是唯一挡 port 0 的地方 ✅
  • settings 解析链与 triage note 一致(provider.baseUrl ?? llm-deepseek.baseURL ?? $DEEPSEEK_BASE_URL ?? 官方默认,仅官方 provider);line-based reader 对 dsh 的 line-based emitter 输出是精确逆映射 ✅
  • launcher:native entry 在场时跳过 --patch overlay(any-profile 规则避免双层重复注册同一模块)✅
  • AGENTS.md §6 双模式检查通过:本 PR 无任何 wire 格式改动;server.ts 改动仅为 instance-file 路由;dsh 流量保持 plain proxy mode,与之前 bili dsh 行为一致 ✅

测试覆盖质量:interceptor 8 个测试(含 race gate、degrade、abort 不重试、gate reject 不挂死)、settings 7 个(含 malformed fall-through)、plugin-install roundtrip 覆盖了幂等/共享 insert group 保留他人条目+组头/stale→broken/no-profiles 报错(tests/plugin-agent.test.ts:954-1014)。WORKLOG 对 e2e 局限(headless one-shot 无法出多轮压缩循环、argv 上限)的披露诚实且合理。

Minor 备注(不阻塞)

  1. stripDshEntries 的组扫描对嵌套列表项不够健壮(src/plugin-install.ts):扫描循环遇到任何非 - 行就 breakif (indentOf(raw) <= parentIndent || !t.startsWith("- ")) break;)。若某个共享 - insert: 组里、我们的条目之后有一个带续行/嵌套键的条目(如 - name: x 下跟 url: y),扫描会提前终止,remaining 误判为 0 → 连组头和其他插件条目一起删掉,破坏别人的 patch 文件。实际暴露面低(dsh 出厂 profile 是裸 [],我们只写单行条目,triage 也对照过真实安装的 dsh),但修法很便宜:把更深缩进的非列表行当作当前条目的续行(skip 而非 break),删除时删整个条目范围而非只删 - name: 行。建议后续小 patch 或跟进 issue。
  2. README 滞后:quickstart 里 bili dsh 那行仍写 "native /acp command injected via --patch",而 bili plugin install dsh 在 README 全文无出处——新的 native 部署路径对用户不可发现。建议补一句(一行即可)。
  3. footnote(给 B/C 复用方)intercept.ts 降级路径若原始 input 是 body 已被首次失败尝试消费的 streaming Request,直连重试会抛 "Body is unusable" 而不是干净降级。pi-ai 发的是 buffered JSON body,今天不会触发;作为共享原语留给 B/C 时留意即可。

判断小结

方案层面:Triage 阶段的审核结论我独立复核后全部成立——resolution chain 打到核心(settings 显式配置优先、env 兜底、不可解析即不拦截),race window 用"同步安装 + per-call 异步解析"关闭是正确的层次(不是治标),per-session port 0 + session instance file 干净地绕开了 #394 而不污染全局注册表。实现与声明一致,e2e 证据充分,可合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

Review vs master 3cd85b6: ⚠️ same family caveat as #523/#524 — parallel implementation, needs consolidation + rework (#521 / family #513).

Good properties (verified):

  • dsh-settings + plugin-install flow for the native deployment is self-contained; devlog REQ/WORKLOG documentation discipline is good.
  • Master src/agent/dsh-acp.ts still exists and the diff is additive against its current shape.

Issues:

  1. Duplicate interception core: feat: dsh 进 PLUGIN_AGENTS——native 插件部署 + 拦截层(native 模式 D) #525's src/agent/intercept.ts (125 lines) vs feat(pi): native mode — self-spawned per-session proxy via in-process fetch interception (#519) #524's (114 lines) — independent implementations of the same mechanism (confirmed NOT stacked: feat: dsh 进 PLUGIN_AGENTS——native 插件部署 + 拦截层(native 模式 D) #525 does not contain feat(pi): native mode — self-spawned per-session proxy via in-process fetch interception (#519) #524's commits). Landing both forks the core; 探索如何发布为native模式 #513 must pick one canonical implementation.
  2. Instance/launcher drift: authored against the pre-fix: instance registry lost-update — per-instance marker files (#527) #528 instance registry and pre-feat(#535): dsh phase 4 — split by destination (last overlay shrinks to loopback) #544 dsh split-by-destination launcher flow (runLaunch for dsh changed in v0.1.85: loopback→httpRewrites, non-loopback https→httpsDomains+MITM, SSL_CERT_FILE combined-CA). The spawn/env plumbing here needs re-validation against that — especially stripInheritedProxy interplay with dsh's LOOPBACK_NO_PROXY policy.
  3. tests/plugin-agent.test.ts (+73) extends a file with known env-dependent flakes — make sure the additions don't inherit them.

Recommendation: hold for family decision; consolidate with #524 on one interception core; rework launcher integration against post-#544 dsh flow.

(tracking: #521 / family #513)

@ranxianglei

Copy link
Copy Markdown
Owner Author

Verified against master 5836f78 (165 commits drifted, zero conflicts — GitHub reports mergeable=True, just "behind"). Test-merged locally:

Pre-flight: typecheck ✓ · 1258/1260 tests (2 known env fails in plugin-agent.test.ts, unrelated; +20 dsh-native tests pass) · build ✓.

No rebase needed — merge as-is.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到。我这边也独立做了轻量复核,与你的结论一致:

  • origin/pr-525 head 仍为 8755ede(自 review 以来未变);origin/master = 5836f78(v0.1.95,merge-base baf1ac5,漂移 165 commits)
  • git merge-tree --write-tree <pr-head> <master> → exit 0,仅输出合并树 OID,零冲突

你本地 test-merge 的 pre-flight(typecheck ✓ · 1258/1260,2 个已知环境依赖失败与改动无关 · build ✓)+ GitHub mergeable=True + 我的零冲突确认,三重一致——无需 rebase,直接合即可。

不过按系统规则 Agent 不能执行 PR 合并(human-only),请你自己点合:

#525

合并后 CI 会跑完整 pre-flight;如需我跟进合并后的验证(如发布流程或 e2e),在 issue 里说一声即可。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: dsh 进 PLUGIN_AGENTS——native 插件部署 + 拦截层(native 模式 D)

1 participant