diff --git a/scripts/proof-73706-message-sending-session-key.ts b/scripts/proof-73706-message-sending-session-key.ts deleted file mode 100644 index 8e643ef8acda..000000000000 --- a/scripts/proof-73706-message-sending-session-key.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Real-runtime behavior proof for #73706. - * - * This script does NOT use vitest mocks. It wires up the production - * `deliverOutboundPayloads` path against: - * - a real `PluginRegistry` populated with one real channel plugin and - * two real plugin hooks (`message_sending`, `message_sent`) - * - the real `getGlobalHookRunner()` / `initializeGlobalHookRunner()` - * singleton path (no fake hook runner) - * - the real `setActivePluginRegistry` channel resolution path (no - * fake channel adapter) - * - * It then exercises three scenarios: - * - * 1. Direct outbound delivery with `session.key` set: confirms the - * `message_sending` and `message_sent` hook contexts both receive - * the canonical `sessionKey`. - * - * 2. Direct outbound delivery with NO session: confirms `sessionKey` - * is absent from both hook contexts (the "narrowed" docs branch). - * - * 3. Native-redirect simulation: outbound delivery whose `session.key` - * is set to the redirect TARGET session (i.e., what the agent - * runtime resolves as `params.sessionKey` when - * `CommandTargetSessionKey` is set and `CommandSource === "native"`, - * and what `dispatch-from-config.ts` now passes through to - * `routeReply`). Confirms `message_sending` / `message_sent` - * observe the redirect-target session, NOT the inbound session. - * This is the runtime invariant Clawsweeper asked us to pin - * with a regression test. - * - * Run with: - * pnpm tsx scripts/proof-73706-message-sending-session-key.ts - */ - -import { deliverOutboundPayloads } from "../src/infra/outbound/deliver.js"; -import type { - PluginHookMessageContext, - PluginHookMessageReceivedEvent, -} from "../src/plugins/hook-message.types.js"; -import { initializeGlobalHookRunner } from "../src/plugins/hook-runner-global.js"; -import { addTestHook, createMockPluginRegistry } from "../src/plugins/hooks.test-helpers.js"; -import type { PluginRegistry } from "../src/plugins/registry.js"; -import { setActivePluginRegistry } from "../src/plugins/runtime.js"; -import { createOutboundTestPlugin, createTestRegistry } from "../src/test-utils/channel-plugins.js"; - -type CapturedContext = { - hook: "message_sending" | "message_sent"; - ctx: PluginHookMessageContext; - event: PluginHookMessageReceivedEvent; -}; - -function buildRegistry(captured: CapturedContext[], channelId: "matrix"): PluginRegistry { - // Real outbound channel plugin: returns a synthetic delivery result - // without touching any network. This drives `deliverOutboundPayloads` - // through its real channel-resolution + sendText path. - const sendText = async () => ({ - channel: channelId, - messageId: `mx-${Date.now()}`, - roomId: "!room:example", - }); - - const channelRegistry = createTestRegistry([ - { - pluginId: channelId, - source: "proof", - plugin: createOutboundTestPlugin({ - id: channelId, - outbound: { deliveryMode: "direct", sendText }, - }), - }, - ]); - - // Real hook handlers: capture exactly what delivery hands to plugins. - const hookRegistry = createMockPluginRegistry([]); - addTestHook({ - registry: hookRegistry, - pluginId: "proof-message-sending", - hookName: "message_sending", - handler: async (event: unknown, ctx: unknown) => { - captured.push({ - hook: "message_sending", - ctx: ctx as PluginHookMessageContext, - event: event as PluginHookMessageReceivedEvent, - }); - // Returning undefined means "do not modify or cancel". - return undefined; - }, - }); - addTestHook({ - registry: hookRegistry, - pluginId: "proof-message-sent", - hookName: "message_sent", - handler: async (event: unknown, ctx: unknown) => { - captured.push({ - hook: "message_sent", - ctx: ctx as PluginHookMessageContext, - event: event as PluginHookMessageReceivedEvent, - }); - }, - }); - - return { - ...channelRegistry, - hooks: hookRegistry.hooks, - typedHooks: hookRegistry.typedHooks, - plugins: hookRegistry.plugins, - }; -} - -async function runScenario( - label: string, - opts: { sessionKey?: string }, -): Promise { - const captured: CapturedContext[] = []; - const registry = buildRegistry(captured, "matrix"); - setActivePluginRegistry(registry); - initializeGlobalHookRunner(registry); - - const result = await deliverOutboundPayloads({ - cfg: {}, - channel: "matrix", - to: "!room:example", - payloads: [{ text: `proof: ${label}` }], - skipQueue: true, - ...(opts.sessionKey ? { session: { key: opts.sessionKey } } : {}), - }); - - console.log(`\n=== Scenario: ${label} ===`); - console.log(`deliverOutboundPayloads result:`, JSON.stringify(result)); - for (const entry of captured) { - console.log( - `[${entry.hook}] ctx.sessionKey = ${ - entry.ctx.sessionKey === undefined ? "(undefined)" : JSON.stringify(entry.ctx.sessionKey) - }`, - ); - console.log(`[${entry.hook}] full ctx = ${JSON.stringify(entry.ctx)}`); - } - - return captured; -} - -async function main() { - console.log("[proof-73706] Real-runtime behavior proof for outbound session-key threading."); - console.log( - "[proof-73706] Production code paths: deliverOutboundPayloads + getGlobalHookRunner.", - ); - - const scenario1 = await runScenario( - "outbound delivery WITH session.key (canonical key from agent runtime)", - { sessionKey: "agent:tank:slack:channel:CHAN1" }, - ); - const scenario2 = await runScenario( - "outbound delivery WITHOUT session (narrowed docs branch)", - {}, - ); - const scenario3 = await runScenario( - "native-redirect: session.key = CommandTargetSessionKey (what dispatch-from-config.ts now passes)", - { sessionKey: "agent:tank:telegram:direct:999" }, - ); - - // Assertions — make the proof self-checking so the captured output is - // not silently green when the runtime regresses. - const expectFromHook = ( - captured: CapturedContext[], - hook: "message_sending" | "message_sent", - expected: string | undefined, - ): void => { - const entry = captured.find((c) => c.hook === hook); - if (!entry) { - throw new Error(`[proof-73706] No ${hook} hook fired.`); - } - if (entry.ctx.sessionKey !== expected) { - throw new Error( - `[proof-73706] ${hook} sessionKey mismatch: expected ${JSON.stringify(expected)} got ${JSON.stringify(entry.ctx.sessionKey)}`, - ); - } - }; - - expectFromHook(scenario1, "message_sending", "agent:tank:slack:channel:CHAN1"); - expectFromHook(scenario1, "message_sent", "agent:tank:slack:channel:CHAN1"); - expectFromHook(scenario2, "message_sending", undefined); - expectFromHook(scenario2, "message_sent", undefined); - expectFromHook(scenario3, "message_sending", "agent:tank:telegram:direct:999"); - expectFromHook(scenario3, "message_sent", "agent:tank:telegram:direct:999"); - - console.log("\n[proof-73706] All runtime assertions passed."); -} - -main().catch((err: unknown) => { - console.error("[proof-73706] FAILED:", err); - process.exitCode = 1; -}); diff --git a/scripts/proof-cron-wake-origin.mts b/scripts/proof-cron-wake-origin.mts deleted file mode 100644 index 52b6a6a12907..000000000000 --- a/scripts/proof-cron-wake-origin.mts +++ /dev/null @@ -1,142 +0,0 @@ -// Live-proof harness for PR #83738 (cron wake origin capture). -// -// Drives the patched gateway wake handler (validateWakeParams + isSubagentSessionKey -// guard) into the patched cron service wake() (wake.ts) with deps wired to LOG -// every enqueueSystemEvent / requestHeartbeat call. Captures stdout that -// demonstrates a non-main cron wake routing to the originating session/agent -// rather than the heartbeat/main default. -// -// Run: pnpm exec tsx scripts/proof-cron-wake-origin.mts -// -// All identifiers in this script are synthetic. Real Telegram chat ids / -// session keys are not used. - -import { cronHandlers } from "../src/gateway/server-methods/cron.js"; -import { wake as cronServiceWake } from "../src/cron/service/wake.js"; -import type { CronServiceState } from "../src/cron/service/state.js"; - -type EnqueueArgs = [string, { sessionKey?: string; agentId?: string } | undefined]; -type HeartbeatArgs = [ - { source: string; intent: string; reason: string; sessionKey?: string; agentId?: string }, -]; - -const log = (...parts: unknown[]) => { - console.log(...parts); -}; - -function makeShimmedState(): { - state: CronServiceState; - recorder: { enqueue: EnqueueArgs[]; heartbeat: HeartbeatArgs[] }; -} { - const recorder = { enqueue: [] as EnqueueArgs[], heartbeat: [] as HeartbeatArgs[] }; - const state = { - deps: { - enqueueSystemEvent: (...args: EnqueueArgs) => { - recorder.enqueue.push(args); - const [text, opts] = args; - log( - `[gateway/cron] enqueueSystemEvent text=${JSON.stringify(text)} opts=${JSON.stringify(opts)}`, - ); - }, - requestHeartbeat: (...args: HeartbeatArgs) => { - recorder.heartbeat.push(args); - log(`[gateway/heartbeat] requestHeartbeat ${JSON.stringify(args[0])}`); - }, - }, - } as unknown as CronServiceState; - return { state, recorder }; -} - -type ScenarioResult = { ok: boolean; payload?: unknown; error?: unknown }; - -async function drive(label: string, params: unknown): Promise { - log(""); - log(`=== ${label} ===`); - log(`> wake params: ${JSON.stringify(params)}`); - const { state } = makeShimmedState(); - let response: ScenarioResult = { ok: false }; - const respond = (ok: boolean, payload: unknown, error: unknown) => { - response = { ok, payload, error }; - }; - const context = { - cron: { - wake: ( - opts: { - mode: "now" | "next-heartbeat"; - text: string; - sessionKey?: string; - agentId?: string; - }, - ) => cronServiceWake(state, opts), - }, - } as unknown as Parameters[0]["context"]; - - // cronHandlers.wake is sync (calls respond synchronously) but typed as - // returning void; await on a Promise wrapper to flush console.log ordering. - await Promise.resolve( - cronHandlers.wake({ - params, - respond, - context, - request: {} as never, - requestId: 1 as never, - logger: undefined as never, - } as never), - ); - log(`< wake result: ok=${response.ok} payload=${JSON.stringify(response.payload)}`); - if (response.error) { - log(`< wake error: ${JSON.stringify(response.error)}`); - } - return response; -} - -async function main() { - log("=== PR #83738 cron wake origin-capture: live-proof harness ==="); - log("Driving the patched gateway wake handler through cron.wake() with"); - log("logging deps. All ids below are synthetic."); - - // Scenario 1: real-world bug-reproduction case — a wake fired from inside - // a non-main Telegram topic session for a non-default agent. - await drive("non-main session + non-default agent (the bug-fix case)", { - mode: "now", - text: "follow up on report", - sessionKey: "agent:coding:telegram::topic:", - agentId: "coding", - }); - - // Scenario 2: backwards-compatible — no origin → default routing. - await drive("no origin (backwards-compatible default routing)", { - mode: "now", - text: "ping", - }); - - // Scenario 3: next-heartbeat + sessionKey collapses to a targeted-immediate - // heartbeat because the regularly-scheduled heartbeat fires for the - // agent's main session, never peeking the targeted lane's queue. - await drive("next-heartbeat + sessionKey collapses to targeted-immediate", { - mode: "next-heartbeat", - text: "check the queue", - sessionKey: "agent:coding:discord:", - agentId: "coding", - }); - - // Scenario 4: subagent sessionKey rejected at the gateway handler. - await drive("subagent sessionKey rejected by gateway handler guard", { - mode: "now", - text: "wake my subagent", - sessionKey: "subagent:scratch:", - }); - - // Scenario 5: whitespace-only origin falls through to default routing. - await drive("whitespace-only origin falls through (defence-in-depth)", { - mode: "now", - text: "x", - sessionKey: " ", - agentId: "\t", - }); - - log(""); - log("=== Done. ==="); -} - -void main(); diff --git a/scripts/repro/cli-web-search-secret-refs-live-proof.mjs b/scripts/repro/cli-web-search-secret-refs-live-proof.mjs deleted file mode 100644 index 508fa5f69281..000000000000 --- a/scripts/repro/cli-web-search-secret-refs-live-proof.mjs +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env node -/** - * Live repro for CLI infer web search SecretRef resolution (PR #82699). - * Run: TAVILY_API_KEY=resolved-live-proof pnpm exec tsx scripts/repro/cli-web-search-secret-refs-live-proof.mjs - */ -import { resolveCommandConfigWithSecrets } from "../../src/cli/command-config-resolution.js"; -import { getCapabilityWebSearchCommandSecretTargetIds } from "../../src/cli/command-secret-targets.js"; - -const unresolvedConfig = { - tools: { web: { search: { provider: "tavily", enabled: true } } }, - plugins: { - entries: { - tavily: { - config: { - webSearch: { - apiKey: { source: "env", provider: "default", id: "TAVILY_API_KEY" }, - }, - }, - }, - }, - }, -}; - -process.env.TAVILY_API_KEY = process.env.TAVILY_API_KEY ?? "resolved-live-proof"; - -const { effectiveConfig, diagnostics } = await resolveCommandConfigWithSecrets({ - config: unresolvedConfig, - commandName: "infer web search", - targetIds: getCapabilityWebSearchCommandSecretTargetIds(), - autoEnable: true, -}); - -const apiKey = effectiveConfig.plugins?.entries?.tavily?.config?.webSearch?.apiKey; -const unresolved = unresolvedConfig.plugins.entries.tavily.config.webSearch.apiKey; - -console.log("unresolved apiKey is SecretRef object =", typeof unresolved === "object"); -console.log( - "resolveCommandConfigWithSecrets apiKey is string =", - typeof apiKey === "string" && apiKey.length > 0, -); -console.log("resolved apiKey remains redacted =", typeof apiKey === "string"); -console.log("diagnostics count =", diagnostics.length); diff --git a/scripts/repro/session-status-run-session-key-live-proof.mjs b/scripts/repro/session-status-run-session-key-live-proof.mjs deleted file mode 100644 index 28a337f108e7..000000000000 --- a/scripts/repro/session-status-run-session-key-live-proof.mjs +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env node -/** - * Live repro for implicit session_status + runSessionKey (#82669 / PR #82696). - * Run: pnpm exec tsx scripts/repro/session-status-run-session-key-live-proof.mjs - */ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-status-proof-")); -const configPath = path.join(tmpRoot, "openclaw.json"); -const storePath = path.join(tmpRoot, "sessions.json"); -const store = { - "agent:main:telegram:default:direct:1234": { - sessionId: "s-tg-direct", - updatedAt: 5, - status: "done", - thinkingLevel: "off", - }, - "agent:main:main": { - sessionId: "s-main", - updatedAt: 10, - status: "running", - thinkingLevel: "high", - }, -}; -fs.writeFileSync(configPath, "{}\n"); -fs.writeFileSync(storePath, `${JSON.stringify(store, null, 2)}\n`); -process.env.OPENCLAW_CONFIG_PATH = configPath; -process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1"; - -const originalStderrWrite = process.stderr.write.bind(process.stderr); -process.stderr.write = (chunk, encoding, callback) => { - const text = String(chunk); - if (text.includes("gateway connect failed:")) { - if (typeof encoding === "function") { - encoding(); - } else if (typeof callback === "function") { - callback(); - } - return true; - } - return originalStderrWrite(chunk, encoding, callback); -}; - -const { createSessionStatusTool } = await import("../../src/agents/tools/session-status-tool.ts"); - -const config = { - session: { mainKey: "main", scope: "per-sender", store: storePath }, - agents: { - defaults: { - model: { primary: "proof/gpt-5.4" }, - models: {}, - }, - }, - tools: { - agentToAgent: { enabled: false }, - }, -}; - -try { - const tool = createSessionStatusTool({ - agentSessionKey: "agent:main:telegram:default:direct:1234", - runSessionKey: "agent:main:main", - config, - }); - - const result = await tool.execute("live-proof-implicit-run-session", {}); - const text = - typeof result === "string" - ? result - : (result.content.find((item) => item.type === "text")?.text ?? result.details.statusText); - const thinkingMatch = text.match(/\bThink:\s+(\w+)/i); - const sessionKey = typeof result === "string" ? undefined : result.details.sessionKey; - - if (sessionKey !== "agent:main:main") { - throw new Error(`expected details.sessionKey agent:main:main, got ${String(sessionKey)}`); - } - if (thinkingMatch?.[1] !== "high") { - throw new Error(`expected status text to mention Think: high, got ${thinkingMatch?.[1]}`); - } - - console.log( - "implicit session_status resolved thinkingLevel from store =", - store["agent:main:main"].thinkingLevel, - ); - console.log("status text mentions thinking:", thinkingMatch[1]); - console.log("details.sessionKey =", sessionKey); - console.log("--- status excerpt ---"); - console.log(text.split("\n").slice(0, 8).join("\n")); -} finally { - process.stderr.write = originalStderrWrite; - fs.rmSync(tmpRoot, { recursive: true, force: true }); -}