From 6ec814ab41d65184e06a99c3f2ba8bc6799d2b11 Mon Sep 17 00:00:00 2001 From: fivetaku Date: Mon, 9 Nov 2026 19:23:59 +0900 Subject: [PATCH 1/4] fix(advisor): resolve the factory for the Claude replay config The Advisor factory slot is keyed by the live config object, but /v1/messages hands the Responses pipeline a freshly spread replay config (buildClaudeReplayConfig plus cacheRetention). The lookup in createRegisteredAdvisorPlan therefore never matched on that path: Claude Code clients got no synthetic advisor tool and no preflight consultation even with the Advisor enabled, while /v1/chat/completions worked. Link request-scoped config views to their live config in a WeakMap and resolve the live factory at lookup time, so disable/re-activation still applies and nothing is stored on the config object (it stays cloneable). Co-Authored-By: Claude Assisted-by: claude-code:claude-ocx-teamclaude--claude-opus-5-5 Agent-Trace: da2ff2a0-1c04-49d7-a520-c190d1055028 --- .../test-layout/layout-agent-assertion.json | 1 + src/server/claude-messages.ts | 6 +- src/server/responses/advisor-plan-slot.ts | 16 +++- structure/advisor.md | 3 + .../advisor-messages-replay-config.test.ts | 73 +++++++++++++++++++ tests/fixtures/test-layout-expected.json | 1 + .../helpers/messages-overflow-differential.ts | 4 +- tests/helpers/messages-proxy-refusal-case.ts | 4 +- 8 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 tests/advisor/advisor-messages-replay-config.test.ts diff --git a/scripts/test-layout/layout-agent-assertion.json b/scripts/test-layout/layout-agent-assertion.json index 190baeb209e..0fd249faf55 100644 --- a/scripts/test-layout/layout-agent-assertion.json +++ b/scripts/test-layout/layout-agent-assertion.json @@ -10,6 +10,7 @@ "advisor-caller-policy-routing.test.ts": "advisor", "advisor-caller-policy-host-key.test.ts": "advisor", "advisor-caller-policy-self-fetch.test.ts": "advisor", + "advisor-messages-replay-config.test.ts": "advisor", "windows-native-principal.test.ts": "windows", "fixture-windows-runtime-preload.test.ts": "ci-workflows", "agent-assertion-auth.test.ts": "server", diff --git a/src/server/claude-messages.ts b/src/server/claude-messages.ts index da26632962e..3a0fc6728d2 100644 --- a/src/server/claude-messages.ts +++ b/src/server/claude-messages.ts @@ -46,6 +46,7 @@ import { captureAdmissionPolicy } from "./capture-admission-policy"; import type { CapturedAdmissionPolicy } from "../types/admission-policy"; import { captureClaudeInbound } from "../claude/inbound-debug"; import { declaredAnthropicServerTools, replayedAnthropicServerBlocks, systemCachePrefix } from "../claude/inbound-content-options"; +import { linkAdvisorPlanConfig } from "./responses/advisor-plan-slot"; import { claudeCodeForIngress } from "../claude/intercept/model-bindings"; import { classifyInterceptClient } from "../claude/intercept/client-class"; import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility"; @@ -1265,12 +1266,13 @@ async function handleClaudeMessagesWithBudget( // BEFORE translation (the translated Anthropic stream has no response.completed // frame, so tapping it records a bogus 502 with no usage/cache detail). const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish; - const upstream = await handleResponses(internalReq, { + // The replay view is a new object; link it so the Advisor factory registered on `config` applies. + const upstream = await handleResponses(internalReq, linkAdvisorPlanConfig(config, { ...buildClaudeReplayConfig(config), // Operator config wins; otherwise the caller's explicit TTLs (all 1h, or a pinned 5m) carry // over, and TTL-less breakpoints take the destination default. cacheRetention: config.cacheRetention ?? callerCacheRetention(anthropicBody), - }, logCtx, { + }), logCtx, { ...(nativeComboSource && envelope && settledRoute?.combo ? { protocolSource: (await import("./messages-native")).createNativeMessagesComboSource({ req, config, logCtx, envelope, requestedModel, translatorBudget, selector: nativeSelector, diff --git a/src/server/responses/advisor-plan-slot.ts b/src/server/responses/advisor-plan-slot.ts index 55174d00c6e..38a6c4374bf 100644 --- a/src/server/responses/advisor-plan-slot.ts +++ b/src/server/responses/advisor-plan-slot.ts @@ -29,6 +29,20 @@ export function setAdvisorPlanFactory(config: OcxConfig, factory: AdvisorPlanFac else factories.delete(config); } +/** Request-scoped config views (e.g. the Claude replay config) -> the live config they were built from. */ +const views = new WeakMap(); + +/** + * Record that `view` is a request-scoped copy of `live`. A view resolves the live config's + * factory at lookup time, so a later disable or re-activation of the live config still applies. + * Nothing is stored on the config object itself: it stays cloneable (#7974). + */ +export function linkAdvisorPlanConfig(live: OcxConfig, view: T): T { + views.set(view, views.get(live) ?? live); + return view; +} + export function createRegisteredAdvisorPlan(input: AdvisorPlanInput): Promise | null { - return factories.get(input.config)?.(input) ?? null; + const live = views.get(input.config); + return (factories.get(input.config) ?? (live && factories.get(live)))?.(input) ?? null; } diff --git a/structure/advisor.md b/structure/advisor.md index dbaf0bfdf56..04f13cba142 100644 --- a/structure/advisor.md +++ b/structure/advisor.md @@ -19,6 +19,9 @@ The server composition root registers it synchronously at startup; a successful updates activation, including enable after startup and disable/reset. The runtime is loaded only when an active factory is used, so its process-local ledger is not constructed by an inactive install. `sidecar-execution.ts` reads the core-owned slot, not Advisor settings or runtime. +Request-scoped copies of the live config, such as the Claude replay config `/v1/messages` passes to +the Responses pipeline, are linked with `linkAdvisorPlanConfig` so they resolve the live config's +factory at lookup time (`tests/advisor/advisor-messages-replay-config.test.ts`). Advisor settings admit the complete synchronous candidate/read/patch/save/activation phase through the [config admission contract](config.md). Production uses the current disk candidate when present, preserving unrelated disk edits during admission. Successful saves adopt the committed Advisor subtree and its field-scoped live baseline together, including known absence after reset, without advancing unrelated pending live edits. Fallback or error-bearing diagnostics are refused before mutation. Deadline refusal leaves live and durable settings untouched and retains the existing `write_failed` response. Successful receipts are serialized in that callback before another settings mutation can run. Management loads `advisor-routes.ts` only for the Advisor namespace. The guard is attached through `_advisorGuard`. `tests/advisor/advisor-core-boundary.test.ts` enforces the transitive load-time diff --git a/tests/advisor/advisor-messages-replay-config.test.ts b/tests/advisor/advisor-messages-replay-config.test.ts new file mode 100644 index 00000000000..480a7322acc --- /dev/null +++ b/tests/advisor/advisor-messages-replay-config.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from "bun:test"; +import { handleClaudeMessages } from "../../src/server/claude-messages"; +import { + createRegisteredAdvisorPlan, linkAdvisorPlanConfig, setAdvisorPlanFactory, type AdvisorPlanInput, +} from "../../src/server/responses/advisor-plan-slot"; +import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; +import type { OcxConfig } from "../../src/types"; + +// The Advisor factory is registered on the live config object, but /v1/messages hands the +// Responses pipeline a request-scoped replay view (`buildClaudeReplayConfig` plus cacheRetention). +// Looking the factory up by that view alone found nothing, so Claude Code never saw the advisor +// tool and preflight never ran while /v1/chat/completions on the same config worked. + +const workerStream = 'data: {"choices":[{"delta":{"content":"fixture"},"finish_reason":null}]}\n\n' + + 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n'; + +const plan = (seen: AdvisorPlanInput[]) => async (input: AdvisorPlanInput) => { + seen.push(input); + return { + tool: { name: "advisor", description: "fixture", parameters: { type: "object", properties: {} }, advisor: true }, + preflightInject: async () => false, + attachGuard: () => {}, + }; +}; + +test("a linked request view resolves the live factory at lookup time", async () => { + const live = { port: 10100, providers: {} } as OcxConfig; + const view = linkAdvisorPlanConfig(live, { ...live, cacheRetention: "short" as const }); + const nested = linkAdvisorPlanConfig(view, { ...view }); + const seen: AdvisorPlanInput[] = []; + expect(createRegisteredAdvisorPlan({ config: view, workerIdentity: "w", workerModelId: "w" })).toBeNull(); + setAdvisorPlanFactory(live, plan(seen)); + try { + expect((await createRegisteredAdvisorPlan({ config: view, workerIdentity: "w", workerModelId: "w" }))?.tool.name).toBe("advisor"); + expect((await createRegisteredAdvisorPlan({ config: nested, workerIdentity: "w", workerModelId: "w" }))?.tool.name).toBe("advisor"); + // The plan still receives the view it was asked for, not the live object. + expect(seen.map(input => input.config)).toEqual([view, nested]); + expect(seen[0]!.config).toBe(view); + // An unrelated copy never inherits a factory it was not linked to. + expect(createRegisteredAdvisorPlan({ config: { ...live }, workerIdentity: "w", workerModelId: "w" })).toBeNull(); + } finally { setAdvisorPlanFactory(live, null); } + // Disabling the live config detaches every view linked to it. + expect(createRegisteredAdvisorPlan({ config: view, workerIdentity: "w", workerModelId: "w" })).toBeNull(); +}); + +test("/v1/messages reaches the factory registered on the live config and sends the advisor tool", async () => { + const release = acquireOwnedSpendHome(); + const workerBodies: Record[] = []; + const seen: AdvisorPlanInput[] = []; + const config: OcxConfig = { + defaultProvider: "worker", + providers: { worker: { adapter: "openai-chat", apiKey: "fixture", models: ["m"], liveModels: false, + baseUrl: "https://worker.test/v1", + fetch: (async (_url, init) => { + workerBodies.push(JSON.parse(String(init?.body))); + return new Response(workerStream, { headers: { "content-type": "text/event-stream" } }); + }) as typeof fetch } }, + }; + setAdvisorPlanFactory(config, plan(seen)); + try { + const response = await handleClaudeMessages(new Request("http://localhost/v1/messages", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "worker/m", max_tokens: 100, stream: false, messages: [{ role: "user", content: "fixture" }] }), + }), config, { model: "", provider: "" }, { requestId: "advisor-messages", start: Date.now() }); + await response.text(); + expect(response.status).toBe(200); + expect(seen).toHaveLength(1); + expect(seen[0]!.config).not.toBe(config); + expect(workerBodies).toHaveLength(1); + const tools = (workerBodies[0]!.tools ?? []) as { function?: { name?: string } }[]; + expect(tools.map(tool => tool.function?.name)).toContain("advisor"); + } finally { setAdvisorPlanFactory(config, null); release(); } +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 79f6f670799..0e9b91d5c6e 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -22,6 +22,7 @@ "advisor-caller-policy-routing.test.ts": "advisor", "advisor-caller-policy-host-key.test.ts": "advisor", "advisor-caller-policy-self-fetch.test.ts": "advisor", + "advisor-messages-replay-config.test.ts": "advisor", "graceful-max-output-tokens.test.ts": "responses", "graceful-max-output-tokens-exclusions.test.ts": "responses", "windows-config-root-async-writers.test.ts": "windows", diff --git a/tests/helpers/messages-overflow-differential.ts b/tests/helpers/messages-overflow-differential.ts index 94ba3473029..d1c0e796f7d 100644 --- a/tests/helpers/messages-overflow-differential.ts +++ b/tests/helpers/messages-overflow-differential.ts @@ -26,8 +26,8 @@ function replaceOnce(source: string, needle: string, replacement: string): strin for (const path of changed) { let source = base ? git("show", `72e80d280:${path}`) : readFileSync(repoPath(path), "utf8"); if (path === "src/server/claude-messages.ts") source = replaceOnce(source, - "const upstream = await handleResponses(internalReq, {", - "const upstream = globalThis.__messagesReplayResponse ? globalThis.__messagesReplayResponse() : await handleResponses(internalReq, {"); + "const upstream = await handleResponses(internalReq, ", + "const upstream = globalThis.__messagesReplayResponse ? globalThis.__messagesReplayResponse() : await handleResponses(internalReq, "); if (path === "src/server/claude-messages.ts") source = replaceOnce(source, "message = await collectAnthropicMessage(anthropicSse,", "if (globalThis.__messagesCollectorStream) await anthropicSse.cancel();\n message = await collectAnthropicMessage(globalThis.__messagesCollectorStream ?? anthropicSse,"); diff --git a/tests/helpers/messages-proxy-refusal-case.ts b/tests/helpers/messages-proxy-refusal-case.ts index 0d03d70f10b..5a44b2c0f58 100644 --- a/tests/helpers/messages-proxy-refusal-case.ts +++ b/tests/helpers/messages-proxy-refusal-case.ts @@ -48,8 +48,8 @@ Bun.plugin({ name: "Messages refusal boundary fixture", setup(build) { build.onLoad({ filter: filters.upstream }, ({ path }) => { hooks.upstreamLoads++; return { loader: "ts", contents: replaceOnce(readOriginal(path), - "const upstream = await handleResponses(internalReq, {", - "const upstream = globalThis.__localRefusalReply ? (globalThis.__fixtureHooks.upstreamHits++, globalThis.__localRefusalReply) : await handleResponses(internalReq, {") }; + "const upstream = await handleResponses(internalReq, ", + "const upstream = globalThis.__localRefusalReply ? (globalThis.__fixtureHooks.upstreamHits++, globalThis.__localRefusalReply) : await handleResponses(internalReq, ") }; }); build.onLoad({ filter: filters.native }, ({ path }) => { hooks.nativeLoads++; From dece91f965e9e2aeb7a344c457bed0371ecb54a1 Mon Sep 17 00:00:00 2001 From: fivetaku Date: Mon, 9 Nov 2026 20:24:59 +0900 Subject: [PATCH 2/4] fix(advisor): read Advisor settings from the live config in replay views Management writes replace the live config's advisor object, so the shallow Messages replay view kept the copied reference and a consent revocation after the copy did not reach the plan. The linked view now reads `advisor` through to the live config; other request-scoped overrides are unchanged and the view stays structuredClone-able. - test: the Messages request runs the preflight pass; consent revoked on the live config after the copy blocks the consultation. - docs: list translated /v1/messages as a supported Advisor client path with the same policy and consent rules. Co-Authored-By: Claude Assisted-by: claude-code:claude-ocx-teamclaude--claude-opus-5-5 Agent-Trace: 657cca3c-0740-4292-871c-c1a96c1ae4c9 --- .../docs/reference/configuration/advisor.md | 4 ++ src/server/responses/advisor-plan-slot.ts | 11 +++- structure/advisor.md | 4 +- .../advisor-messages-replay-config.test.ts | 61 +++++++++++++++++-- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/advisor.md b/docs-site/src/content/docs/reference/configuration/advisor.md index 36c557abf5d..891a6021777 100644 --- a/docs-site/src/content/docs/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/reference/configuration/advisor.md @@ -206,6 +206,10 @@ consultation never switches the session's main model. - Native OpenAI passthrough turns (ChatGPT-pool workers) do not get the synthetic tool; advisor support covers routed (translated) providers. Preflight consultation applies to run-turn adapters; the tool does not. +- Translated `/v1/messages` requests (for example Claude Code routed to a translated provider) + get the synthetic tool and preflight like `/v1/responses` and `/v1/chat/completions`. The + same `policy` and context-sharing consent rules apply, and the settings are read from the live + config, so a consent revocation also blocks consultations for requests already in flight. - No semantic stuck detection, completion review, escalation tiers, multiple advisors or voting. Adaptive consultation requires the structured validation/edit/retry evidence described above. - The preflight dedup ledger is process-local; after a proxy restart, a task in progress may diff --git a/src/server/responses/advisor-plan-slot.ts b/src/server/responses/advisor-plan-slot.ts index 38a6c4374bf..d128e1dee3c 100644 --- a/src/server/responses/advisor-plan-slot.ts +++ b/src/server/responses/advisor-plan-slot.ts @@ -35,10 +35,17 @@ const views = new WeakMap(); /** * Record that `view` is a request-scoped copy of `live`. A view resolves the live config's * factory at lookup time, so a later disable or re-activation of the live config still applies. - * Nothing is stored on the config object itself: it stays cloneable (#7974). + * The view's `advisor` reads through to the live config: management writes replace that object + * (`config.advisor = ...`), so a copied reference would keep a revoked consent alive for the + * plan. Every other request-scoped override on the view is kept. Nothing is stored on the live + * config, and an accessor is still cloneable (#7974). */ export function linkAdvisorPlanConfig(live: OcxConfig, view: T): T { - views.set(view, views.get(live) ?? live); + const root = views.get(live) ?? live; + views.set(view, root); + if (view !== root) { + Object.defineProperty(view, "advisor", { get: () => root.advisor, enumerable: true, configurable: true }); + } return view; } diff --git a/structure/advisor.md b/structure/advisor.md index 04f13cba142..f68d450429d 100644 --- a/structure/advisor.md +++ b/structure/advisor.md @@ -21,7 +21,9 @@ when an active factory is used, so its process-local ledger is not constructed b install. `sidecar-execution.ts` reads the core-owned slot, not Advisor settings or runtime. Request-scoped copies of the live config, such as the Claude replay config `/v1/messages` passes to the Responses pipeline, are linked with `linkAdvisorPlanConfig` so they resolve the live config's -factory at lookup time (`tests/advisor/advisor-messages-replay-config.test.ts`). +factory at lookup time. The view's `advisor` reads through to the live config, so a consent +revocation after the copy still blocks outbound transfer +(`tests/advisor/advisor-messages-replay-config.test.ts`). Advisor settings admit the complete synchronous candidate/read/patch/save/activation phase through the [config admission contract](config.md). Production uses the current disk candidate when present, preserving unrelated disk edits during admission. Successful saves adopt the committed Advisor subtree and its field-scoped live baseline together, including known absence after reset, without advancing unrelated pending live edits. Fallback or error-bearing diagnostics are refused before mutation. Deadline refusal leaves live and durable settings untouched and retains the existing `write_failed` response. Successful receipts are serialized in that callback before another settings mutation can run. Management loads `advisor-routes.ts` only for the Advisor namespace. The guard is attached through `_advisorGuard`. `tests/advisor/advisor-core-boundary.test.ts` enforces the transitive load-time diff --git a/tests/advisor/advisor-messages-replay-config.test.ts b/tests/advisor/advisor-messages-replay-config.test.ts index 480a7322acc..8987d496de2 100644 --- a/tests/advisor/advisor-messages-replay-config.test.ts +++ b/tests/advisor/advisor-messages-replay-config.test.ts @@ -1,10 +1,18 @@ -import { expect, test } from "bun:test"; +import { afterEach, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { createAdvisorRuntimePlan } from "../../src/advisor/runtime"; +import { createAdvisorTaskBudget } from "../../src/advisor/budget"; +import { createAdvisorActivityStore } from "../../src/advisor/activity"; +import { createAdvisorPreflightLedger } from "../../src/advisor/state"; +import { parseRequest } from "../../src/responses/parser"; import { handleClaudeMessages } from "../../src/server/claude-messages"; import { createRegisteredAdvisorPlan, linkAdvisorPlanConfig, setAdvisorPlanFactory, type AdvisorPlanInput, } from "../../src/server/responses/advisor-plan-slot"; import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; -import type { OcxConfig } from "../../src/types"; +import type { OcxConfig, OcxParsedRequest } from "../../src/types"; // The Advisor factory is registered on the live config object, but /v1/messages hands the // Responses pipeline a request-scoped replay view (`buildClaudeReplayConfig` plus cacheRetention). @@ -14,11 +22,18 @@ import type { OcxConfig } from "../../src/types"; const workerStream = 'data: {"choices":[{"delta":{"content":"fixture"},"finish_reason":null}]}\n\n' + 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n'; -const plan = (seen: AdvisorPlanInput[]) => async (input: AdvisorPlanInput) => { +const originalFetch = globalThis.fetch; +const roots: string[] = []; +afterEach(() => { + globalThis.fetch = originalFetch; + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +const plan = (seen: AdvisorPlanInput[], preflights: OcxParsedRequest[] = []) => async (input: AdvisorPlanInput) => { seen.push(input); return { tool: { name: "advisor", description: "fixture", parameters: { type: "object", properties: {} }, advisor: true }, - preflightInject: async () => false, + preflightInject: async (parsed: OcxParsedRequest) => { preflights.push(parsed); return false; }, attachGuard: () => {}, }; }; @@ -36,6 +51,8 @@ test("a linked request view resolves the live factory at lookup time", async () // The plan still receives the view it was asked for, not the live object. expect(seen.map(input => input.config)).toEqual([view, nested]); expect(seen[0]!.config).toBe(view); + // The read-through accessor keeps the view cloneable (#7974). + expect(structuredClone(view).cacheRetention).toBe("short"); // An unrelated copy never inherits a factory it was not linked to. expect(createRegisteredAdvisorPlan({ config: { ...live }, workerIdentity: "w", workerModelId: "w" })).toBeNull(); } finally { setAdvisorPlanFactory(live, null); } @@ -47,6 +64,7 @@ test("/v1/messages reaches the factory registered on the live config and sends t const release = acquireOwnedSpendHome(); const workerBodies: Record[] = []; const seen: AdvisorPlanInput[] = []; + const preflights: OcxParsedRequest[] = []; const config: OcxConfig = { defaultProvider: "worker", providers: { worker: { adapter: "openai-chat", apiKey: "fixture", models: ["m"], liveModels: false, @@ -56,7 +74,7 @@ test("/v1/messages reaches the factory registered on the live config and sends t return new Response(workerStream, { headers: { "content-type": "text/event-stream" } }); }) as typeof fetch } }, }; - setAdvisorPlanFactory(config, plan(seen)); + setAdvisorPlanFactory(config, plan(seen, preflights)); try { const response = await handleClaudeMessages(new Request("http://localhost/v1/messages", { method: "POST", headers: { "content-type": "application/json" }, @@ -66,8 +84,41 @@ test("/v1/messages reaches the factory registered on the live config and sends t expect(response.status).toBe(200); expect(seen).toHaveLength(1); expect(seen[0]!.config).not.toBe(config); + // The Messages turn runs the automatic preflight pass, not only the tool declaration. + expect(preflights).toHaveLength(1); + expect(JSON.stringify(preflights[0]!.context.messages)).toContain("fixture"); expect(workerBodies).toHaveLength(1); const tools = (workerBodies[0]!.tools ?? []) as { function?: { name?: string } }[]; expect(tools.map(tool => tool.function?.name)).toContain("advisor"); } finally { setAdvisorPlanFactory(config, null); release(); } }); + +test("consent revoked on the live config after the view was copied blocks the consultation", async () => { + const root = mkdtempSync(join(tmpdir(), "advisor-messages-consent-")); roots.push(root); + const calls: unknown[] = []; + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + calls.push(init?.body); + return Response.json({ choices: [{ message: { content: "advice" } }] }); + }) as typeof fetch; + const live = { port: 10100, providers: {}, advisor: { + enabled: true, model: "expert", policy: "manual", effort: "high", contextSharingConsent: "v1", + } } as OcxConfig; + const view = linkAdvisorPlanConfig(live, { ...live, cacheRetention: "short" as const }); + setAdvisorPlanFactory(live, async input => createAdvisorRuntimePlan({ + ...input, baseUrlOverride: "http://expert.test", ledger: createAdvisorPreflightLedger(), + budget: createAdvisorTaskBudget(join(root, "budget.sqlite")), activities: createAdvisorActivityStore(), + })); + try { + const registered = await createRegisteredAdvisorPlan({ config: view, workerIdentity: "w", workerModelId: "w" }); + expect(registered?.consult).toBeDefined(); + // Management writes replace the live object (`config.advisor = ...`) rather than mutating it. + const { contextSharingConsent: _revoked, ...rest } = live.advisor!; + live.advisor = rest; + const parsed = parseRequest({ model: "w", stream: false, input: [{ role: "user", content: "private-task-text" }] }); + const result = await registered!.consult!(parsed, "manual", "question"); + expect(result.blocked).toBe("consent"); + expect(calls).toHaveLength(0); + // Request-scoped overrides on the view are untouched. + expect(view.cacheRetention).toBe("short"); + } finally { setAdvisorPlanFactory(live, null); } +}); From a088865a03fb5f512ed05562f5da238c46c1f8c6 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 10 Nov 2026 18:43:49 +0900 Subject: [PATCH 3/4] fix(advisor): record translated consultations under live config Resolve request-scoped replay views to the live config for activity storage, and clarify the boundary of consent revocation after dispatch. Exercise the translated request, management activity feed, and consent timing. Co-authored-by: fivetaku --- .../docs/reference/configuration/advisor.md | 4 +- src/advisor/runtime.ts | 6 +- src/server/responses/advisor-plan-slot.ts | 13 +- structure/advisor.md | 8 +- .../advisor-messages-replay-config.test.ts | 162 +++++++++++++++++- 5 files changed, 186 insertions(+), 7 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/advisor.md b/docs-site/src/content/docs/reference/configuration/advisor.md index 891a6021777..e27dd07d19a 100644 --- a/docs-site/src/content/docs/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/reference/configuration/advisor.md @@ -209,7 +209,9 @@ consultation never switches the session's main model. - Translated `/v1/messages` requests (for example Claude Code routed to a translated provider) get the synthetic tool and preflight like `/v1/responses` and `/v1/chat/completions`. The same `policy` and context-sharing consent rules apply, and the settings are read from the live - config, so a consent revocation also blocks consultations for requests already in flight. + config, so a consent revocation blocks consultations for requests already in flight that have + not yet been dispatched. It does not cancel a dispatched consultation or retract context that + already left the proxy. - No semantic stuck detection, completion review, escalation tiers, multiple advisors or voting. Adaptive consultation requires the structured validation/edit/retry evidence described above. - The preflight dedup ledger is process-local; after a proxy restart, a task in progress may diff --git a/src/advisor/runtime.ts b/src/advisor/runtime.ts index 82c3e6d1287..6af97c74126 100644 --- a/src/advisor/runtime.ts +++ b/src/advisor/runtime.ts @@ -36,6 +36,7 @@ import { } from "./state"; import { formatAdvisorAdvice, advisorPreflightMessages, formatAdvisorUnavailable } from "./context"; import { advisorActivityStore, type AdvisorActivityStore, type AdvisorTrigger } from "./activity"; +import { advisorPlanRootConfig } from "../server/responses/advisor-plan-slot"; import { createAdvisorTaskBudget, type AdvisorTaskBudget } from "./budget"; import { collectAdvisorRepairEvidence, type AdvisorRepairEvidence } from "./evidence"; import type { AdmissionModelScope } from "../server/admission-model-scope"; @@ -95,7 +96,10 @@ export function createAdvisorRuntimePlan(deps: AdvisorRuntimeDeps): AdvisorRunti const ledger = deps.ledger ?? sharedPreflightLedger; const now = deps.now ?? (() => Date.now()); const budget = deps.budget ?? createAdvisorTaskBudget(); - const activities = deps.activities ?? advisorActivityStore(deps.config); + // Identity-scoped state must key on the LIVE config: `GET /api/advisor/activity` reads the store + // of the management context's own config, so a request-scoped view (the Claude replay config) + // would record consultations the dashboard and CLI never see. + const activities = deps.activities ?? advisorActivityStore(advisorPlanRootConfig(deps.config)); // Management PUT mutates this same config object (`config.advisor = ...`). Re-resolving // at dispatch is what makes a mid-request consent revocation stop outbound transfer. const liveSettings = () => resolveAdvisorSettings(deps.config); diff --git a/src/server/responses/advisor-plan-slot.ts b/src/server/responses/advisor-plan-slot.ts index d128e1dee3c..eea097b8ebf 100644 --- a/src/server/responses/advisor-plan-slot.ts +++ b/src/server/responses/advisor-plan-slot.ts @@ -30,7 +30,8 @@ export function setAdvisorPlanFactory(config: OcxConfig, factory: AdvisorPlanFac } /** Request-scoped config views (e.g. the Claude replay config) -> the live config they were built from. */ -const views = new WeakMap(); +type AdvisorLinkedConfig = Pick; +const views = new WeakMap(); /** * Record that `view` is a request-scoped copy of `live`. A view resolves the live config's @@ -49,6 +50,16 @@ export function linkAdvisorPlanConfig(live: OcxConfig, view return view; } +/** + * The live config a request-scoped view was linked to, or the config itself when it was not + * linked. Identity-scoped state that must outlive one request (the activity feed the management + * route reads from its own config object) resolves through the same link as the factory. + */ +export function advisorPlanRootConfig(config: T): T | OcxConfig { + return views.get(config) ?? config; +} + +/** Create the plan from the factory registered for this config, or for the live config it was linked to. */ export function createRegisteredAdvisorPlan(input: AdvisorPlanInput): Promise | null { const live = views.get(input.config); return (factories.get(input.config) ?? (live && factories.get(live)))?.(input) ?? null; diff --git a/structure/advisor.md b/structure/advisor.md index f68d450429d..97716650227 100644 --- a/structure/advisor.md +++ b/structure/advisor.md @@ -22,7 +22,9 @@ install. `sidecar-execution.ts` reads the core-owned slot, not Advisor settings Request-scoped copies of the live config, such as the Claude replay config `/v1/messages` passes to the Responses pipeline, are linked with `linkAdvisorPlanConfig` so they resolve the live config's factory at lookup time. The view's `advisor` reads through to the live config, so a consent -revocation after the copy still blocks outbound transfer +revocation after the copy still blocks outbound transfer up to dispatch: the runtime rechecks +consent immediately before the loopback call, and a revocation after that call has been dispatched +neither cancels it nor retracts context already sent (`tests/advisor/advisor-messages-replay-config.test.ts`). Advisor settings admit the complete synchronous candidate/read/patch/save/activation phase through the [config admission contract](config.md). Production uses the current disk candidate when present, preserving unrelated disk edits during admission. Successful saves adopt the committed Advisor subtree and its field-scoped live baseline together, including known absence after reset, without advancing unrelated pending live edits. Fallback or error-bearing diagnostics are refused before mutation. Deadline refusal leaves live and durable settings untouched and retains the existing `write_failed` response. Successful receipts are serialized in that callback before another settings mutation can run. Management loads `advisor-routes.ts` only for the Advisor namespace. The guard is attached through @@ -337,7 +339,9 @@ request under the advisor model. Consultation usage is never merged into the wor usage; intercepted worker legs are, via the same usage-merge rule the terminal guard applies. `src/advisor/activity.ts` holds config-scoped process memory: at most 100 records, at most 24 -hours old, with pruning on access and no timer. `GET /api/advisor/activity` and `ocx advisor +hours old, with pruning on access and no timer. The store is keyed by the LIVE config object, so a +request that runs on a linked view (the Claude replay config) records into the feed the management +route reads. `GET /api/advisor/activity` and `ocx advisor activity` expose trigger, status, models, requested/mapped effort, duration and reported tokens. There are no task bodies, commands, advice, conversation identifiers or provider error bodies in these records. Dashboard polling runs only while its page is visible. Restart clears activity, diff --git a/tests/advisor/advisor-messages-replay-config.test.ts b/tests/advisor/advisor-messages-replay-config.test.ts index 8987d496de2..95a4e5e09b3 100644 --- a/tests/advisor/advisor-messages-replay-config.test.ts +++ b/tests/advisor/advisor-messages-replay-config.test.ts @@ -2,12 +2,15 @@ import { afterEach, expect, test } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { createAdvisorRuntimePlan } from "../../src/advisor/runtime"; +import { createAdvisorRuntimePlan, type AdvisorRuntimeDeps } from "../../src/advisor/runtime"; import { createAdvisorTaskBudget } from "../../src/advisor/budget"; import { createAdvisorActivityStore } from "../../src/advisor/activity"; import { createAdvisorPreflightLedger } from "../../src/advisor/state"; import { parseRequest } from "../../src/responses/parser"; import { handleClaudeMessages } from "../../src/server/claude-messages"; +import { handleChatCompletions } from "../../src/server/chat-completions"; +import { handleAdvisorRoutes } from "../../src/server/management/advisor-routes"; +import type { ManagementContext } from "../../src/server/management/context"; import { createRegisteredAdvisorPlan, linkAdvisorPlanConfig, setAdvisorPlanFactory, type AdvisorPlanInput, } from "../../src/server/responses/advisor-plan-slot"; @@ -22,6 +25,12 @@ import type { OcxConfig, OcxParsedRequest } from "../../src/types"; const workerStream = 'data: {"choices":[{"delta":{"content":"fixture"},"finish_reason":null}]}\n\n' + 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n'; +/** Distinctive advice so the dispatched worker body proves what the preflight pass delivered. */ +const ADVICE = "advise from the translated messages fixture"; + +/** Distinctive advice so the worker body proves it carried the preflight payload. */ +const STUB_ADVICE = '{"advisor_result":{"status":"advice","advice":"stub-advice-marker"}}'; + const originalFetch = globalThis.fetch; const roots: string[] = []; afterEach(() => { @@ -33,7 +42,14 @@ const plan = (seen: AdvisorPlanInput[], preflights: OcxParsedRequest[] = []) => seen.push(input); return { tool: { name: "advisor", description: "fixture", parameters: { type: "object", properties: {} }, advisor: true }, - preflightInject: async (parsed: OcxParsedRequest) => { preflights.push(parsed); return false; }, + preflightInject: async (parsed: OcxParsedRequest) => { + preflights.push(parsed); + parsed.context.messages = [ + ...parsed.context.messages, + { role: "user", content: STUB_ADVICE, timestamp: Date.now() }, + ]; + return true; + }, attachGuard: () => {}, }; }; @@ -90,6 +106,8 @@ test("/v1/messages reaches the factory registered on the live config and sends t expect(workerBodies).toHaveLength(1); const tools = (workerBodies[0]!.tools ?? []) as { function?: { name?: string } }[]; expect(tools.map(tool => tool.function?.name)).toContain("advisor"); + // The advice the preflight pass injected reached the dispatch, not just the tool declaration. + expect(JSON.stringify(workerBodies[0])).toContain("stub-advice-marker"); } finally { setAdvisorPlanFactory(config, null); release(); } }); @@ -122,3 +140,143 @@ test("consent revoked on the live config after the view was copied blocks the co expect(view.cacheRetention).toBe("short"); } finally { setAdvisorPlanFactory(live, null); } }); + +/** The management activity route as the dashboard and CLI reach it: it reads its own config object. */ +async function managementActivity(config: OcxConfig) { + const ctx: ManagementContext = { + req: new Request("http://localhost/api/advisor/activity"), + url: new URL("http://localhost/api/advisor/activity"), + config, + deps: {}, + version: "test", + trustedLoopbackIngress: false, + guiSessionIssuance: null, + convergeCodexCatalog: async () => ({ status: "skipped", reason: "not-requested", retryable: false }), + syncClaudeAgentDefsBestEffort: async () => {}, + }; + const response = await handleAdvisorRoutes(ctx); + return (await response!.json()) as { entries: { trigger: string; status: string; advisorModel: string }[] }; +} + +/** A real translated /v1/messages turn: worker provider "worker", Advisor routed to "expert". */ +function translatedAdvisorFixture(options: { revokeOnDispatch?: boolean } = {}) { + const root = mkdtempSync(join(tmpdir(), "advisor-messages-fixture-")); roots.push(root); + const workerBodies: Record[] = []; + const expertBodies: { url: string; body: Record }[] = []; + let revoked = false; + const config: OcxConfig = { + defaultProvider: "worker", + advisor: { enabled: true, model: "expert/worker", policy: "preflight", effort: "high", contextSharingConsent: "v1" }, + providers: { + worker: { adapter: "openai-chat", apiKey: "fixture", models: ["m"], liveModels: false, + baseUrl: "https://worker.test/v1", + fetch: (async (_url, init) => { + workerBodies.push(JSON.parse(String(init?.body))); + return new Response(workerStream, { headers: { "content-type": "text/event-stream" } }); + }) as typeof fetch }, + expert: { adapter: "openai-chat", apiKey: "fixture", models: ["worker"], liveModels: false, + baseUrl: "https://expert.test/v1", + fetch: (async (url, init) => { + // Provider-facing model id is the target model ("worker"); the destination proves the route. + expertBodies.push({ url: String(url), body: JSON.parse(String(init?.body)) }); + // A revocation that lands only after the outbound hop has begun. + if (options.revokeOnDispatch) revoke(); + return Response.json({ choices: [{ message: { content: ADVICE } }], + usage: { prompt_tokens: 11, completion_tokens: 7, total_tokens: 18 } }); + }) as typeof fetch }, + }, + } as OcxConfig; + const revoke = () => { + if (revoked) return; + revoked = true; + // Management writes replace the object; nothing mutates the copied reference in place. + const { contextSharingConsent: _revokedConsent, ...rest } = config.advisor!; + config.advisor = rest; + }; + const register = (extra: Partial = {}) => setAdvisorPlanFactory(config, async input => createAdvisorRuntimePlan({ + ...input, + ledger: createAdvisorPreflightLedger(), + budget: createAdvisorTaskBudget(join(root, "budget.sqlite")), + ...extra, + })); + // The Advisor self-fetches over loopback; re-enter the real data plane so the expert provider runs. + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.includes("/v1/chat/completions")) { + return await handleChatCompletions(new Request(url, init), config, { model: "", provider: "" }); + } + throw new Error("unexpected external fetch: " + url); + }) as typeof fetch; + return { config, workerBodies, expertBodies, revoke, register, revoked: () => revoked }; +} + +/** The real Messages request, with orientation evidence after the latest user turn. */ +function messagesRequest(): Request { + return new Request("http://localhost/v1/messages", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "worker/m", max_tokens: 100, stream: false, messages: [ + { role: "user", content: [{ type: "text", text: "repair the token store" }, + { type: "tool_result", tool_use_id: "call_1", content: "3 tests failed" }] }, + ] }), + signal: AbortSignal.timeout(10_000), + }); +} + +// Regression: the plan used to take its activity store from the replay view. Stores are keyed by +// config identity, and the management route reads the live config's store, so a consultation made +// through /v1/messages was invisible in the dashboard and the CLI activity feed. +test("/v1/messages records its consultation in the management-visible activity feed", async () => { + const release = acquireOwnedSpendHome(); + const fixture = translatedAdvisorFixture(); + fixture.register(); + try { + const response = await handleClaudeMessages(messagesRequest(), + fixture.config, { model: "", provider: "" }, { requestId: "advisor-messages-activity", start: Date.now() }); + await response.text(); + expect(response.status).toBe(200); + expect(fixture.expertBodies).toHaveLength(1); + expect(fixture.expertBodies[0]!.url).toBe("https://expert.test/v1/chat/completions"); + + const feed = await managementActivity(fixture.config); + expect(feed.entries).toHaveLength(1); + expect(feed.entries[0]).toMatchObject({ trigger: "preflight", status: "ok", advisorModel: "expert/worker" }); + // The feed's record is the consultation whose advice reached the worker. + expect(JSON.stringify(fixture.workerBodies[0])).toContain(ADVICE); + } finally { setAdvisorPlanFactory(fixture.config, null); release(); } +}); + +// Consent is rechecked before the outbound hop. A revocation in the claim-to-dispatch window +// blocks the transfer; the worker still runs without advisory context. +test("revocation after the view was copied but before dispatch blocks the /v1/messages consultation", async () => { + const release = acquireOwnedSpendHome(); + const fixture = translatedAdvisorFixture(); + fixture.register({ afterPreflightClaim: fixture.revoke }); + try { + const response = await handleClaudeMessages(messagesRequest(), + fixture.config, { model: "", provider: "" }, { requestId: "advisor-messages-revoke-before", start: Date.now() }); + await response.text(); + expect(fixture.revoked()).toBe(true); + expect(response.status).toBe(200); + expect(fixture.expertBodies).toHaveLength(0); + expect(fixture.workerBodies).toHaveLength(1); + expect(JSON.stringify(fixture.workerBodies[0])).not.toContain(ADVICE); + } finally { setAdvisorPlanFactory(fixture.config, null); release(); } +}); + +// The documented boundary: once dispatched, a revocation neither cancels the consultation nor +// retracts context already sent. This is what the docs now say, instead of promising a retraction. +test("revocation after dispatch does not cancel the /v1/messages consultation", async () => { + const release = acquireOwnedSpendHome(); + const fixture = translatedAdvisorFixture({ revokeOnDispatch: true }); + fixture.register(); + try { + const response = await handleClaudeMessages(messagesRequest(), + fixture.config, { model: "", provider: "" }, { requestId: "advisor-messages-revoke-after", start: Date.now() }); + await response.text(); + expect(fixture.revoked()).toBe(true); + expect(response.status).toBe(200); + // Dispatched: the context left the proxy, and the consultation completed. + expect(fixture.expertBodies).toHaveLength(1); + expect(JSON.stringify(fixture.workerBodies[0])).toContain(ADVICE); + } finally { setAdvisorPlanFactory(fixture.config, null); release(); } +}); From 93037c7c2a0ba3fcd0e77d69058edac379240039 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 10 Nov 2026 20:33:36 +0900 Subject: [PATCH 4/4] docs(advisor): state revocation's real limit in the translations The seven translated Advisor pages said `ocx advisor consent --revoke` stops sending immediately. The English page states the actual boundary: a revocation blocks consultations that have not been dispatched yet, and a dispatched consultation cannot be cancelled nor can context that already left the proxy be retracted. Translate that distinction in ko, ja, zh-cn, zh-tw, fr, ru and tr without changing surrounding wording or facts. --- .../src/content/docs/fr/reference/configuration/advisor.md | 2 +- .../src/content/docs/ja/reference/configuration/advisor.md | 2 +- .../src/content/docs/ko/reference/configuration/advisor.md | 2 +- .../src/content/docs/ru/reference/configuration/advisor.md | 2 +- .../src/content/docs/tr/reference/configuration/advisor.md | 2 +- .../src/content/docs/zh-cn/reference/configuration/advisor.md | 2 +- .../src/content/docs/zh-tw/reference/configuration/advisor.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/configuration/advisor.md b/docs-site/src/content/docs/fr/reference/configuration/advisor.md index de932d06d6b..391b556d058 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/fr/reference/configuration/advisor.md @@ -43,7 +43,7 @@ Sur la page **Advisor**, choisissez le modèle dans le catalogue. Le sélecteur Gérez-le via la page **Advisor** du tableau de bord ou `ocx advisor status|on|off|set --model --effort --policy `. -Sans consentement courant, `ocx advisor on` n'active pas le partage inter-fournisseurs : il affiche cette divulgation et s'arrête. `ocx advisor on --ack-context-sharing` et `ocx advisor consent` enregistrent `v1`. `ocx advisor consent --revoke` retire le consentement et arrête immédiatement l'envoi. `ocx advisor set` n'accorde pas le consentement. La case du tableau de bord n'est pas précochée. +Sans consentement courant, `ocx advisor on` n'active pas le partage inter-fournisseurs : il affiche cette divulgation et s'arrête. `ocx advisor on --ack-context-sharing` et `ocx advisor consent` enregistrent `v1`. `ocx advisor consent --revoke` retire le consentement. Les consultations qui n'ont pas encore été envoyées sont bloquées, mais une consultation déjà envoyée ne peut pas être annulée, et le contexte déjà transmis hors du proxy ne peut pas être retiré. `ocx advisor set` n'accorde pas le consentement. La case du tableau de bord n'est pas précochée. ## Politiques diff --git a/docs-site/src/content/docs/ja/reference/configuration/advisor.md b/docs-site/src/content/docs/ja/reference/configuration/advisor.md index 59b4614e1ed..304ccdd9723 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/ja/reference/configuration/advisor.md @@ -35,7 +35,7 @@ description: manual、preflight、証拠に基づく adaptive ポリシーによ ダッシュボードの **Advisor** ページまたは `ocx advisor status|on|off|set --model --effort --policy ` で管理します。 -現在の同意がないとき、`ocx advisor on` はプロバイダー間送信を有効にしません。開示を表示して停止します。`ocx advisor on --ack-context-sharing` と `ocx advisor consent` が `v1` を記録します。`ocx advisor consent --revoke` は同意を消し、送信を直ちに止めます。`ocx advisor set` は同意を与えません。ダッシュボードの同意チェックは初期状態でオフです。 +現在の同意がないとき、`ocx advisor on` はプロバイダー間送信を有効にしません。開示を表示して停止します。`ocx advisor on --ack-context-sharing` と `ocx advisor consent` が `v1` を記録します。`ocx advisor consent --revoke` は同意を消します。まだディスパッチされていない相談はブロックされますが、すでにディスパッチされた相談は取り消せず、すでにプロキシを離れた文脈も取り戻せません。`ocx advisor set` は同意を与えません。ダッシュボードの同意チェックは初期状態でオフです。 ## ポリシー diff --git a/docs-site/src/content/docs/ko/reference/configuration/advisor.md b/docs-site/src/content/docs/ko/reference/configuration/advisor.md index dd7c22c8114..22c2dad1369 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/ko/reference/configuration/advisor.md @@ -35,7 +35,7 @@ description: manual, preflight 및 증거 기반 adaptive 정책을 제공하는 대시보드 **Advisor** 페이지 또는 `ocx advisor status|on|off|set --model --effort --policy `로 관리합니다. -현재 동의가 없으면 `ocx advisor on`은 프로바이더 간 전송을 켜지 않습니다. 고지를 출력하고 멈춥니다. `ocx advisor on --ack-context-sharing`와 `ocx advisor consent`가 `v1`을 기록합니다. `ocx advisor consent --revoke`는 동의를 지우고 전송을 즉시 멈춥니다. `ocx advisor set`은 동의를 부여하지 않습니다. 대시보드의 동의 확인란은 미리 선택되지 않습니다. +현재 동의가 없으면 `ocx advisor on`은 프로바이더 간 전송을 켜지 않습니다. 고지를 출력하고 멈춥니다. `ocx advisor on --ack-context-sharing`와 `ocx advisor consent`가 `v1`을 기록합니다. `ocx advisor consent --revoke`는 동의를 지웁니다. 아직 디스패치되지 않은 상담은 차단되지만, 이미 디스패치된 상담은 취소할 수 없고 이미 프록시를 떠난 맥락도 되돌릴 수 없습니다. `ocx advisor set`은 동의를 부여하지 않습니다. 대시보드의 동의 확인란은 미리 선택되지 않습니다. ## 정책 diff --git a/docs-site/src/content/docs/ru/reference/configuration/advisor.md b/docs-site/src/content/docs/ru/reference/configuration/advisor.md index 30b9a518175..2cd2076ca11 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/ru/reference/configuration/advisor.md @@ -35,7 +35,7 @@ description: Независимые консультации с политика Управляйте через страницу **Advisor** на дашборде или `ocx advisor status|on|off|set --model --effort --policy `. -Без текущего согласия `ocx advisor on` не включает межпровайдерную отправку: команда печатает раскрытие и останавливается. `ocx advisor on --ack-context-sharing` и `ocx advisor consent` записывают `v1`. `ocx advisor consent --revoke` снимает согласие и сразу прекращает отправку. `ocx advisor set` согласие не выдаёт. Флажок на дашборде изначально снят. +Без текущего согласия `ocx advisor on` не включает межпровайдерную отправку: команда печатает раскрытие и останавливается. `ocx advisor on --ack-context-sharing` и `ocx advisor consent` записывают `v1`. `ocx advisor consent --revoke` снимает согласие. Ещё не отправленные консультации блокируются, но уже отправленную консультацию нельзя отменить, а ушедший из прокси контекст нельзя отозвать. `ocx advisor set` согласие не выдаёт. Флажок на дашборде изначально снят. ## Политики diff --git a/docs-site/src/content/docs/tr/reference/configuration/advisor.md b/docs-site/src/content/docs/tr/reference/configuration/advisor.md index b371679afc1..519d2717671 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/tr/reference/configuration/advisor.md @@ -35,7 +35,7 @@ Kontrol panelindeki **Advisor** sayfasında katalogdan model seçin. Yerel, sağ Panodaki **Advisor** sayfası veya `ocx advisor status|on|off|set --model --effort --policy ` ile yönetin. -Güncel onay yokken `ocx advisor on` sağlayıcılar arası gönderimi açmaz: açıklamayı basar ve durur. `ocx advisor on --ack-context-sharing` ile `ocx advisor consent` `v1` kaydeder. `ocx advisor consent --revoke` onayı kaldırır ve gönderimi hemen durdurur. `ocx advisor set` onay vermez. Panodaki onay kutusu önceden işaretli değildir. +Güncel onay yokken `ocx advisor on` sağlayıcılar arası gönderimi açmaz: açıklamayı basar ve durur. `ocx advisor on --ack-context-sharing` ile `ocx advisor consent` `v1` kaydeder. `ocx advisor consent --revoke` onayı kaldırır. Henüz gönderilmemiş danışmalar engellenir, ancak gönderilmiş bir danışma iptal edilemez ve proxy'den ayrılmış bağlam geri alınamaz. `ocx advisor set` onay vermez. Panodaki onay kutusu önceden işaretli değildir. ## Politikalar diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md b/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md index da488791a18..d0b3f324abe 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md @@ -35,7 +35,7 @@ description: 独立专家咨询,支持 manual、preflight 与基于证据的 a 通过仪表盘的 **Advisor** 页面或 `ocx advisor status|on|off|set --model --effort --policy ` 管理。 -`ocx advisor on` 在没有当前同意时不会开启跨 provider 发送:它会打印披露并停止。`ocx advisor on --ack-context-sharing` 与 `ocx advisor consent` 记录 `v1`。`ocx advisor consent --revoke` 会移除同意并立即停止发送。`ocx advisor set` 不授予同意。仪表盘上的同意复选框默认不勾选。 +`ocx advisor on` 在没有当前同意时不会开启跨 provider 发送:它会打印披露并停止。`ocx advisor on --ack-context-sharing` 与 `ocx advisor consent` 记录 `v1`。`ocx advisor consent --revoke` 会移除同意。尚未发出的咨询会被阻断,但已经发出的咨询无法取消,已经离开代理的上下文也无法收回。`ocx advisor set` 不授予同意。仪表盘上的同意复选框默认不勾选。 ## 策略 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md b/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md index 917cb4b8bc6..68d4d849814 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md @@ -35,7 +35,7 @@ description: 獨立專家諮詢,支援 manual、preflight 與依據證據的 a 透過儀表板的 **Advisor** 頁面或 `ocx advisor status|on|off|set --model --effort --policy ` 管理。 -沒有目前同意時,`ocx advisor on` 不會開啟跨 provider 傳送:它會印出揭露並停止。`ocx advisor on --ack-context-sharing` 與 `ocx advisor consent` 記錄 `v1`。`ocx advisor consent --revoke` 會移除同意並立即停止傳送。`ocx advisor set` 不授予同意。儀表板上的同意核取方塊預設不勾選。 +沒有目前同意時,`ocx advisor on` 不會開啟跨 provider 傳送:它會印出揭露並停止。`ocx advisor on --ack-context-sharing` 與 `ocx advisor consent` 記錄 `v1`。`ocx advisor consent --revoke` 會移除同意。尚未發出的諮詢會被阻擋,但已經發出的諮詢無法取消,已經離開代理的上下文也無法收回。`ocx advisor set` 不授予同意。儀表板上的同意核取方塊預設不勾選。 ## 策略