diff --git a/docs/branch-disposition.md b/docs/branch-disposition.md index d526131..0860664 100644 --- a/docs/branch-disposition.md +++ b/docs/branch-disposition.md @@ -962,7 +962,7 @@ Goose v1.49 本地 canary 收口、Context Runtime + Agent Harness 融合 Phase 审计日期:2026-09-10 分支 HEAD:`47d0d2ac` -`origin/main` 对应提交:`47d0d2ac`(待 push) +`origin/main` 对应提交:`938f6d6d` ### 原始用途 @@ -978,3 +978,28 @@ fusion-plan §6.6 本机开发工具:`codebase-memory-mcp` 本地安装验证 - 保留本地分支名用于审计追溯。 - **不要** merge、cherry-pick 或从该分支继续开发。 +## `feature/dsh-executor-local-setup` + +**状态:禁止再次引用。改动已提交并进入 `origin/main`,该分支保留仅用于只读追溯,不是待合并开发分支。** + +审计日期:2026-09-10 +分支 HEAD:(见 merge commit) +`origin/main` 对应提交:(见 merge commit) + +### 原始用途 + +`@deepseek-ai/dsh@0.1.1-rc.2` 本机安装、check 脚本、Tool Gateway headless E2E;Context Runtime shadow Portal E2E(events-only 省 token)。 + +### 验证摘要 + +- `dsh --version`:0.1.1-rc.2 +- `node scripts/check-dsh-executor-local.mjs`:`DSH_EXECUTOR_CLI_OK` / dry-run OK +- `MEMIND_DSH_RUN_LIVE_SMOKE=1` headless:OK(列 3 文件) +- `MEMIND_TOOL_GATEWAY_DSH_ENABLED=1 node scripts/test-dsh-executor-gateway-e2e.mjs`:`DSH_GATEWAY_E2E_OK` +- Shadow Portal(`force_deep_reasoning`,run `2ca8be7b…`):`headroom_context_observed` + `context_budget_resolved`,`mode=shadow` + +### 最终处置 + +- 保留本地分支名用于审计追溯。 +- **不要** merge、cherry-pick 或从该分支继续开发。 + diff --git a/docs/context-runtime-local-dev.md b/docs/context-runtime-local-dev.md index bdffaf5..bffc71d 100644 --- a/docs/context-runtime-local-dev.md +++ b/docs/context-runtime-local-dev.md @@ -64,6 +64,18 @@ node scripts/check-codebase-memory-mcp-local.mjs node scripts/check-dsh-executor-local.mjs ``` +## Shadow Portal E2E(1 轮,省 token 默认只验事件) + +Portal 需带 shadow 五件套(见上文)运行 `pnpm dev`: + +```bash +node scripts/run-context-runtime-shadow-portal-e2e.mjs +# 默认 CONTEXT_RUNTIME_SHADOW_EVENTS_ONLY=1:拿到 headroom/budget shadow 事件即停,不等 Goose 跑完 +# 通过 → CONTEXT_RUNTIME_SHADOW_PORTAL_OK +``` + +`recall_fusion_resolved` 在 agent memory 关闭时可能缺失,脚本会 WARN 但不阻断。 + ## 用户自测清单(有 LLM 配额时) 1. 确认 **未设置** `GOOSE_V149_ALLOW_REAL_LLM=1` 除非你明确要跑 live smoke diff --git a/docs/dsh-executor-local.md b/docs/dsh-executor-local.md index 3cfefe0..578fdc4 100644 --- a/docs/dsh-executor-local.md +++ b/docs/dsh-executor-local.md @@ -62,6 +62,15 @@ node scripts/check-dsh-executor-local.mjs - **预计超过 10k token 必须先经用户同意** - 禁止循环重跑、禁止 unattended `check-goosed-v149-all` / phase3 聚合 +## Tool Gateway E2E(一次 headless) + +```bash +MEMIND_TOOL_GATEWAY_DSH_ENABLED=1 node scripts/test-dsh-executor-gateway-e2e.mjs +# → DSH_GATEWAY_E2E_OK +``` + +路由 `repo_refactor` → `dsh`;会消耗 LLM token,须人工批准。 + ## headless 手动探针 ```bash diff --git a/scripts/run-context-runtime-shadow-portal-e2e.mjs b/scripts/run-context-runtime-shadow-portal-e2e.mjs new file mode 100644 index 0000000..905691e --- /dev/null +++ b/scripts/run-context-runtime-shadow-portal-e2e.mjs @@ -0,0 +1,232 @@ +#!/usr/bin/env node +/** + * Context Runtime shadow portal E2E: one agent turn, verify shadow events in DB. + * Default: poll shadow events only (do not wait for full Goose completion — saves LLM tokens). + */ +import { randomUUID } from 'node:crypto'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createDbPool } from '../db.mjs'; +import { CONTEXT_RUNTIME_SHADOW_ENV } from '../context-runtime-profile.mjs'; +import { + createReporter, + loginViaApi, + resolvePortalBase, + sleep, + waitForRunTerminal, +} from './scenario-test-lib.mjs'; +import { waitForAgentRunWorkerIdle } from './goose-v149-worker-idle.mjs'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + +for (const [key, value] of Object.entries(CONTEXT_RUNTIME_SHADOW_ENV)) { + process.env[key] = process.env[key] ?? value; +} +process.env.HEADROOM_OUTPUT_SHAPER = process.env.HEADROOM_OUTPUT_SHAPER ?? '0'; + +const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081)); +const timeoutMs = Number(process.env.CONTEXT_RUNTIME_SHADOW_PORTAL_TIMEOUT_MS ?? 180_000); +const eventsOnly = process.env.CONTEXT_RUNTIME_SHADOW_EVENTS_ONLY !== '0'; +const pollMs = Number(process.env.CONTEXT_RUNTIME_SHADOW_POLL_MS ?? 2000); + +async function portalReachable() { + try { + const response = await fetch(`${baseUrl}/auth/status`); + return response.ok; + } catch { + return false; + } +} + +async function fetchRunEvents(pool, runId) { + const [rows] = await pool.query( + `SELECT event_type, data_json, created_at + FROM h5_agent_run_events + WHERE run_id = ? + ORDER BY created_at ASC`, + [runId], + ); + return rows.map((row) => ({ + eventType: row.event_type, + data: typeof row.data_json === 'string' ? JSON.parse(row.data_json) : row.data_json, + createdAt: Number(row.created_at), + })); +} + +function pickEvents(events, type) { + return events.filter((event) => event.eventType === type); +} + +function evaluateShadowEvents(events) { + const headroomEvents = pickEvents(events, 'headroom_context_observed'); + const budgetEvents = pickEvents(events, 'context_budget_resolved'); + const fusionEvents = pickEvents(events, 'recall_fusion_resolved'); + const issues = []; + const warnings = []; + + if (!headroomEvents.length) issues.push('missing headroom_context_observed'); + if (!budgetEvents.length) issues.push('missing context_budget_resolved'); + if (!fusionEvents.length) { + warnings.push('missing recall_fusion_resolved (memory path may be off/skipped)'); + } + + for (const [label, rows] of [ + ['headroom', headroomEvents], + ['budget', budgetEvents], + ['fusion', fusionEvents], + ]) { + const mode = rows[0]?.data?.mode; + if (mode && mode !== 'shadow') { + issues.push(`${label} mode=${mode} (expected shadow)`); + } + } + + return { headroomEvents, budgetEvents, fusionEvents, issues, warnings }; +} + +async function pollShadowEvents(pool, runId, deadlineMs) { + while (Date.now() < deadlineMs) { + const events = await fetchRunEvents(pool, runId); + const evaluation = evaluateShadowEvents(events); + const coreReady = evaluation.headroomEvents.length && evaluation.budgetEvents.length; + if (coreReady) { + return { events, evaluation, done: true }; + } + await sleep(pollMs); + } + const events = await fetchRunEvents(pool, runId); + return { events, evaluation: evaluateShadowEvents(events), done: false }; +} + +async function runPortalTurn(pool) { + const username = process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john'; + const password = + process.env.JOHN_PASSWORD + ?? process.env.H5_ACCESS_PASSWORD + ?? process.env.MEMIND_PASSWORD + ?? ''; + if (!password) throw new Error('set JOHN_PASSWORD for portal e2e'); + + const reporter = createReporter(); + const auth = await loginViaApi(baseUrl, { username, password }, reporter); + await waitForAgentRunWorkerIdle(root, process.env, { + logPrefix: '[context-runtime-shadow-e2e]', + }); + + const startRes = await fetch(`${baseUrl}/api/agent/start`, { + method: 'POST', + headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + const started = await startRes.json().catch(() => ({})); + if (!startRes.ok || !started?.id) { + throw new Error(`agent/start failed: ${startRes.status}`); + } + const sessionId = started.id; + + const warmResume = await fetch(`${baseUrl}/api/agent/resume`, { + method: 'POST', + headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + session_id: sessionId, + load_model_and_extensions: true, + }), + }); + if (!warmResume.ok) { + const warmBody = await warmResume.text().catch(() => ''); + throw new Error(`pre-run resume failed: ${warmResume.status} ${warmBody.slice(0, 200)}`); + } + + const requestId = randomUUID(); + const runRes = await fetch(`${baseUrl}/api/agent/runs`, { + method: 'POST', + headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + request_id: requestId, + session_id: sessionId, + force_deep_reasoning: true, + user_message: { + id: randomUUID(), + role: 'user', + content: [{ type: 'text', text: '帮我搜索一下今天上海的天气,简要回答即可' }], + metadata: { + userVisible: true, + agentVisible: true, + displayText: 'context runtime shadow agent ping', + }, + }, + }), + }); + const runPayload = await runRes.json().catch(() => ({})); + if (!runRes.ok) { + throw new Error(`POST /api/agent/runs ${runRes.status}: ${JSON.stringify(runPayload).slice(0, 300)}`); + } + const runId = runPayload.run?.id ?? runPayload.id; + + if (eventsOnly) { + const polled = await pollShadowEvents(pool, runId, Date.now() + Math.min(timeoutMs, 60_000)); + return { + sessionId, + runId, + terminal: { status: polled.done ? 'shadow_events_ready' : 'shadow_events_timeout' }, + events: polled.events, + evaluation: polled.evaluation, + eventsOnly: true, + }; + } + + const terminal = await waitForRunTerminal(baseUrl, auth.cookie, runId, timeoutMs); + const events = await fetchRunEvents(pool, runId); + return { + sessionId, + runId, + terminal, + events, + evaluation: evaluateShadowEvents(events), + eventsOnly: false, + }; +} + +async function main() { + if (!(await portalReachable())) { + throw new Error(`Portal not reachable at ${baseUrl} — start pnpm dev first`); + } + + const pool = createDbPool(); + const portal = await runPortalTurn(pool); + const { headroomEvents, budgetEvents, fusionEvents, issues, warnings } = portal.evaluation; + + console.log('CONTEXT_RUNTIME_SHADOW_PORTAL_E2E:'); + console.log(` portal=${baseUrl}`); + console.log(` run_id=${portal.runId}`); + console.log(` mode=${portal.eventsOnly ? 'events_only' : 'full_terminal'}`); + console.log(` terminal=${portal.terminal.status}`); + console.log(` headroom_events=${headroomEvents.length}`); + console.log(` budget_events=${budgetEvents.length}`); + console.log(` fusion_events=${fusionEvents.length}`); + if (headroomEvents[0]?.data) { + console.log(` headroom_sample=${JSON.stringify(headroomEvents[0].data)}`); + } + if (budgetEvents[0]?.data) { + console.log(` budget_sample=${JSON.stringify(budgetEvents[0].data)}`); + } + if (fusionEvents[0]?.data) { + console.log(` fusion_sample=${JSON.stringify(fusionEvents[0].data)}`); + } + for (const warning of warnings) { + console.warn(`CONTEXT_RUNTIME_SHADOW_PORTAL_WARN: ${warning}`); + } + + if (issues.length) { + console.error(`CONTEXT_RUNTIME_SHADOW_PORTAL_FAIL: ${issues.join('; ')}`); + process.exit(1); + } + + console.log('CONTEXT_RUNTIME_SHADOW_PORTAL_OK'); +} + +main().catch((err) => { + console.error(err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/scripts/test-dsh-executor-gateway-e2e.mjs b/scripts/test-dsh-executor-gateway-e2e.mjs new file mode 100644 index 0000000..02aec60 --- /dev/null +++ b/scripts/test-dsh-executor-gateway-e2e.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Minimal Tool Gateway → dsh executor E2E (one headless task, costs LLM tokens). + * + * Usage: + * MEMIND_TOOL_GATEWAY_DSH_ENABLED=1 \ + * node scripts/test-dsh-executor-gateway-e2e.mjs + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { buildDshExecutorLaunchPlan } from '../dsh-agent-launch.mjs'; +import { createToolGateway } from '../tool-gateway.mjs'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const cwd = process.argv[2] ?? root; +const instruction = process.env.MEMIND_DSH_GATEWAY_TASK + ?? 'List three files in the workspace root and stop. Do not modify any files.'; + +process.env.MEMIND_TOOL_GATEWAY_ENABLED = process.env.MEMIND_TOOL_GATEWAY_ENABLED ?? '1'; +process.env.MEMIND_TOOL_GATEWAY_DRY_RUN = '0'; +process.env.MEMIND_TOOL_GATEWAY_DSH_ENABLED = process.env.MEMIND_TOOL_GATEWAY_DSH_ENABLED ?? '1'; +process.env.MEMIND_TOOL_GATEWAY_DSH_TASK_TYPES = process.env.MEMIND_TOOL_GATEWAY_DSH_TASK_TYPES ?? 'repo_refactor'; + +const gateway = createToolGateway({ + env: process.env, + llmProviderService: { + async getExecutorLaunchPlan(executor, options) { + if (executor !== 'dsh') { + throw new Error(`unexpected executor ${executor}`); + } + return buildDshExecutorLaunchPlan({ + cwd: options.cwd, + instruction: options.instruction, + env: process.env, + runtimeEnv: { + DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? process.env.OPENAI_API_KEY ?? '', + }, + }); + }, + }, +}); + +const status = gateway.getStatus(); +console.log('DSH_GATEWAY_E2E_PROBE:'); +console.log(JSON.stringify({ + enabled: status.enabled, + dryRun: status.dryRun, + executors: status.executors, + dshEnabled: status.dshEnabled, + dshTaskTypes: status.dshTaskTypes, + selected: gateway.selectExecutor({ taskType: 'repo_refactor' }), +}, null, 2)); + +if (!status.dshEnabled) { + console.error('DSH_GATEWAY_E2E_FAIL: MEMIND_TOOL_GATEWAY_DSH_ENABLED is off'); + process.exit(1); +} + +const result = await gateway.executeJob({ + runId: 'dsh-gateway-e2e', + requestId: 'dsh-gateway-req', + userId: 'local-user', + cwd, + taskType: 'repo_refactor', + timeoutMs: Number(process.env.MEMIND_DSH_GATEWAY_TIMEOUT_MS ?? 180_000), + userMessage: { + content: [{ type: 'text', text: instruction }], + metadata: { + memindRun: { + taskType: 'repo_refactor', + toolMode: 'code', + }, + }, + }, +}); + +const stdoutPreview = String(result.stdout ?? result.displayStdout ?? '').trim().slice(0, 500); +console.log('DSH_GATEWAY_E2E_RESULT:'); +console.log(JSON.stringify({ + ok: result.ok, + executor: result.executor, + exitCode: result.exitCode, + stdoutPreview, +}, null, 2)); + +if (!result.ok || result.executor !== 'dsh') { + console.error('DSH_GATEWAY_E2E_FAIL'); + process.exit(1); +} + +console.log('DSH_GATEWAY_E2E_OK');