Replies: 3 comments
|
补充可落地的补丁。基于 改动内容
diff --git a/packages/experimental/auto-review/src/index.ts b/packages/experimental/auto-review/src/index.ts
index 16070e5..f147042 100644
--- a/packages/experimental/auto-review/src/index.ts
+++ b/packages/experimental/auto-review/src/index.ts
@@ -614,7 +614,17 @@ async function readDecision(stream: AsyncIterable<StreamChunk>): Promise<AutoRev
return parseDecision(final.text)
}
-/** Review one frozen pending action with the fixed policy and current LLM route. */
+/**
+ * Review one frozen pending action with the fixed policy and current LLM route.
+ *
+ * The review belongs to the Session whose action it authorizes, so it carries
+ * that Session's identity: routing metadata is transport, not review input, and
+ * a provider that routes per conversation refuses an identity-less request
+ * outright. Without it the reviewer cannot reach a gateway such as OpenCode Go
+ * (`x-opencode-session`), every review fails closed, and Auto denies every tool
+ * call in the Session. The reviewer request still copies neither the main
+ * request's route metadata nor its messages.
+ */
async function classifyRisk(
ctx: Context,
agent: Agent,
@@ -632,6 +642,7 @@ async function classifyRisk(
content: [{ type: 'text', text: reviewUserText(snapshot) }],
}],
temperature: 0,
+ sessionId: agent.session.id,
signal,
})
return readDecision(ctx.llm.stream(options))
diff --git a/packages/experimental/auto-review/tests/auto-review.spec.ts b/packages/experimental/auto-review/tests/auto-review.spec.ts
index 4245119..d7d3880 100644
--- a/packages/experimental/auto-review/tests/auto-review.spec.ts
+++ b/packages/experimental/auto-review/tests/auto-review.spec.ts
@@ -385,8 +385,10 @@ describe('native review request', () => {
model: 'same-model',
system: EXPECTED_REVIEW_POLICY,
temperature: 0,
+ // The review runs inside this Session and must carry its routing identity,
+ // or a per-conversation gateway rejects the request and Auto denies every call.
+ sessionId: session.id,
})
- expect(request).not.toHaveProperty('sessionId')
expect(request.maxTokens).toBeUndefined()
expect(request.tools).toBeUndefined()
expect(request.messages).toHaveLength(1)
@@ -477,6 +479,53 @@ describe('native review request', () => {
expect(requestText).not.toContain('obsolete description')
})
+ it('carries the Session identity a per-conversation route requires, so the review is not a blanket denial', async () => {
+ // A gateway that routes per conversation (OpenCode Go) refuses an
+ // identity-less request. The reviewer fails closed, so before the review
+ // carried the Session identity every tool call in such a Session was denied
+ // instead of reviewed. This adapter stands in for that gateway.
+ const seen: (GenerateOptions['sessionId'])[] = []
+ const gateway = (options: GenerateOptions): AsyncIterable<StreamChunk> => {
+ seen.push(options.sessionId)
+ return (async function* () {
+ if (options.sessionId === undefined) {
+ yield {
+ type: 'finish',
+ reason: {
+ kind: 'error',
+ failure: { message: '400: MissingSessionID', code: 'INVALID_REQUEST' },
+ },
+ }
+ return
+ }
+ for (const chunk of decisionChunks('{"risk":"low","decision":"allow"}')) yield chunk
+ })()
+ }
+ const { ctx, adapter } = await harness([gateway])
+ const probe = registerProbe(ctx)
+ const { session, agent } = autoSession(ctx, 'native-session-identity')
+ appendHeader(session, [{ name: 'probe', description: 'probe', parameters: { type: 'object' } }])
+ const callId = ToolCallId('identity-native-call')
+ appendAssistant(session, [
+ { type: 'tool-call', id: callId, name: 'probe', arguments: '{"path":"target"}' },
+ ], 1, 1)
+ appendNativeCall(session, callId, 'probe', '{"path":"target"}', 1, 1)
+
+ const result = await ctx.tools.execute({
+ signal: new AbortController().signal,
+ callId,
+ name: 'probe',
+ arguments: { path: 'target' },
+ agent,
+ })
+
+ expect(seen).toEqual([session.id])
+ expect(result.isError).toBe(false)
+ expect(probe.runs()).toBe(1)
+ expect(adapter.requests).toHaveLength(1)
+ expect(adapter.requests[0]?.sessionId).toBe(session.id)
+ })
+
it('scopes reused native call ids and started prefixes to one assistant step', async () => {
const { ctx, adapter } = await harness([
decisionChunks('{"risk":"low","decision":"allow"}'),为什么现有断言的语义是反的第 389 行的 验证情况
pnpm vitest run packages/experimental/auto-review/tests/auto-review.spec.ts |
复现补充:0.2.0-rc.2 + pi-ai 0.87.1 上的线上证据,以及一个已验证的临时规避(English below ↓) 版本 / Versions:dsh 复现 / Reproduction:与主帖完全一致——会话内每一次工具调用都在执行前被拒: 抓包证据 / Wire-level evidence(把缺口定位到辅助调用点):把该 route 的 只有审阅器那次请求没有带——与主帖的调用点对照表一致: 耗时对照 / Latency corroboration(同一会话、同一 route、同类工具):
多出来的这一趟正是审阅器请求——也就是 400 的那一次。 临时规避(已实测有效)/ Verified stopgap:在 provider profile 上固定该头,使主/辅请求都携带: - id: llm-pi-ai
config:
providers:
opencode-go:
apiKeyEnv: OPENCODE_GO_API_KEY
headers:
x-opencode-session: <stable-id>重启后 Auto review 恢复正常(工具正常执行,审阅器往返体现为延迟)。注意:pi-ai 只在头不存在时才注入,因此这是用"整个 profile 共用一个稳定 id"换取可用性,属于临时规避而非修复;上面评论里的 patch( Corroborating report on 0.2.0-rc.2 (pi-ai 0.87.1): wire-level evidence + a verified stopgapVersions: dsh Reproduction: identical to the OP — every tool call in the session is denied before its body runs, with the same Wire-level evidence (isolates the gap to the auxiliary call site): with the route's Only the reviewer's request omits it — matching the call-site table above ( Latency corroboration (same session, same route, same kind of tool): with manual review (no reviewer call) Verified stopgap: pin the header on the provider profile so both main and auxiliary requests carry it: - id: llm-pi-ai
config:
providers:
opencode-go:
apiKeyEnv: OPENCODE_GO_API_KEY
headers:
x-opencode-session: <stable-id>After a restart, Auto review works again (tools execute; the reviewer round trip shows up as added latency). Caveat: pi-ai injects the header only when it is absent, so this trades per-conversation identity for one stable id per profile — a stopgap, not the fix. The patch in the comment above ( |
Windows x64 /
|
Uh oh!
There was an error while loading. Please reload this page.
Summary
@deepseek-ai/dsh-experimental-auto-review构造审阅请求时没有传sessionId。审阅器因此无法通过"按会话路由"的网关(OpenCode Go 要求x-opencode-session):请求收到400 MissingSessionID,而 Auto review 对技术故障一律 fail closed,于是整个会话里每个工具调用都被拒绝。Environment
0.2.0-rc.1(Windows x64)。opencode-go/deepseek-v4.1-flash(pi-ai ≥0.86.1,即已具备withOpenCodeSessionHeader())。packages/experimental/auto-review/src/index.ts的classifyRisk()。Current behavior
同一个会话里工具调用连续被拒(实测一次会话内 13 次),报错原文:
链路是:
readDecision()在finish.reason.kind === 'error'时抛错 →failed(exec, error)→{ kind: 'deny' }。也就是说审阅器只要不可达,会话内所有工具都停摆,而错误信息看起来像"审阅判定拒绝",很容易被误解为策略问题。Root cause
classifyRisk()的审阅 options 少了会话身份(master 现状):而 DSH 的其它内部调用点都带了:
dsh-agent-loop(主循环request)sessionId: this.session.iddsh-session-title-llmsessionId: request.session.iddsh-compaction-basicsessionId: agent.session.iddsh-experimental-auto-reviewdsh-llm-pi-ai会把GenerateOptions.sessionId透传给 pi-ai(sessionId: String(options.sessionId)),pi-ai 的withOpenCodeSessionHeader()再把它写成x-opencode-session。所以这条链上只差 auto-review 这一处。另外,现有单测把这个错误行为固化成了断言:
tests/auto-review.spec.ts:389→expect(request).not.toHaveProperty('sessionId')。Reproduction
x-opencode-session)。opencode-go/deepseek-v4.1-flash;权限模式选 Auto review。pwsh等)→ 复现上面的 400 + 拒绝。对照(不含 DSH,直接打网关,证明缺头就是 400):
Expected behavior
审阅请求带上它所授权的那次动作所属会话的身份;审阅返回 allow 后工具正常执行。会话身份属于传输层的路由元数据,不是审阅输入,不需要暴露给模型,也不改变审阅策略。
Suggested fix
temperature: 0, + sessionId: agent.session.id, signal,并把
tests/auto-review.spec.ts:389的断言反过来(审阅请求必须带本会话身份),再补一条回归用例:网关在sessionId === undefined时返回400 MissingSessionID,断言工具仍然被执行(而不是被拒绝)。Impact
任何按会话路由/配额/缓存的网关都会命中;表现为"该 provider 下工具完全不能用",且错误文本指向审阅拒绝而非网络问题,排查成本很高。
All reactions