mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 10:51:23 +00:00
fix: preserve inter-session input provenance (thanks @anbecker)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { INPUT_PROVENANCE_KIND_VALUES } from "../../../sessions/input-provenance.js";
|
||||
import { NonEmptyString, SessionLabelString } from "./primitives.js";
|
||||
|
||||
export const AgentEventSchema = Type.Object(
|
||||
@@ -64,6 +65,17 @@ export const AgentParamsSchema = Type.Object(
|
||||
timeout: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lane: Type.Optional(Type.String()),
|
||||
extraSystemPrompt: Type.Optional(Type.String()),
|
||||
inputProvenance: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.String({ enum: [...INPUT_PROVENANCE_KIND_VALUES] }),
|
||||
sourceSessionKey: Type.Optional(Type.String()),
|
||||
sourceChannel: Type.Optional(Type.String()),
|
||||
sourceTool: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
idempotencyKey: NonEmptyString,
|
||||
label: Type.Optional(SessionLabelString),
|
||||
spawnedBy: Type.Optional(Type.String()),
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "../../infra/outbound/agent-delivery.js";
|
||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js";
|
||||
import {
|
||||
@@ -85,6 +86,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
timeout?: number;
|
||||
label?: string;
|
||||
spawnedBy?: string;
|
||||
inputProvenance?: InputProvenance;
|
||||
};
|
||||
const cfg = loadConfig();
|
||||
const idem = request.idempotencyKey;
|
||||
@@ -97,6 +99,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
let resolvedGroupSpace: string | undefined = groupSpaceRaw || undefined;
|
||||
let spawnedByValue =
|
||||
typeof request.spawnedBy === "string" ? request.spawnedBy.trim() : undefined;
|
||||
const inputProvenance = normalizeInputProvenance(request.inputProvenance);
|
||||
const cached = context.dedupe.get(`agent:${idem}`);
|
||||
if (cached) {
|
||||
respond(cached.ok, cached.payload, cached.error, {
|
||||
@@ -400,6 +403,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
runId,
|
||||
lane: request.lane,
|
||||
extraSystemPrompt: request.extraSystemPrompt,
|
||||
inputProvenance,
|
||||
},
|
||||
defaultRuntime,
|
||||
context.deps,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getFreePort,
|
||||
installGatewayTestHooks,
|
||||
startGatewayServer,
|
||||
testState,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
@@ -17,13 +18,15 @@ let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||
let gatewayPort: number;
|
||||
let prevGatewayPort: string | undefined;
|
||||
let prevGatewayToken: string | undefined;
|
||||
const gatewayToken = "test-token";
|
||||
|
||||
beforeAll(async () => {
|
||||
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
|
||||
prevGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
gatewayPort = await getFreePort();
|
||||
testState.gatewayAuth = { mode: "token", token: gatewayToken };
|
||||
process.env.OPENCLAW_GATEWAY_PORT = String(gatewayPort);
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = "test-token";
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
|
||||
server = await startGatewayServer(gatewayPort);
|
||||
});
|
||||
|
||||
@@ -105,8 +108,14 @@ describe("sessions_send gateway loopback", () => {
|
||||
expect(details.reply).toBe("pong");
|
||||
expect(details.sessionKey).toBe("main");
|
||||
|
||||
const firstCall = spy.mock.calls[0]?.[0] as { lane?: string } | undefined;
|
||||
const firstCall = spy.mock.calls[0]?.[0] as
|
||||
| { lane?: string; inputProvenance?: { kind?: string; sourceTool?: string } }
|
||||
| undefined;
|
||||
expect(firstCall?.lane).toBe("nested");
|
||||
expect(firstCall?.inputProvenance).toMatchObject({
|
||||
kind: "inter_session",
|
||||
sourceTool: "sessions_send",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -92,6 +92,27 @@ describe("readFirstUserMessageFromTranscript", () => {
|
||||
expect(result).toBe("First user question");
|
||||
});
|
||||
|
||||
test("skips inter-session user messages by default", () => {
|
||||
const sessionId = "test-session-inter-session";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "user",
|
||||
content: "Forwarded by session tool",
|
||||
provenance: { kind: "inter_session", sourceTool: "sessions_send" },
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
message: { role: "user", content: "Real user message" },
|
||||
}),
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
|
||||
|
||||
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
|
||||
expect(result).toBe("Real user message");
|
||||
});
|
||||
|
||||
test("returns null when no user messages exist", () => {
|
||||
const sessionId = "test-session-4";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveSessionTranscriptPathInDir,
|
||||
} from "../config/sessions.js";
|
||||
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
||||
import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js";
|
||||
import { extractToolCallNames, hasToolCall } from "../utils/transcript-tools.js";
|
||||
import { stripEnvelope } from "./chat-sanitize.js";
|
||||
|
||||
@@ -139,6 +140,7 @@ const MAX_LINES_TO_SCAN = 10;
|
||||
type TranscriptMessage = {
|
||||
role?: string;
|
||||
content?: string | Array<{ type: string; text?: string }>;
|
||||
provenance?: unknown;
|
||||
};
|
||||
|
||||
function extractTextFromContent(content: TranscriptMessage["content"]): string | null {
|
||||
@@ -167,6 +169,7 @@ export function readFirstUserMessageFromTranscript(
|
||||
storePath: string | undefined,
|
||||
sessionFile?: string,
|
||||
agentId?: string,
|
||||
opts?: { includeInterSession?: boolean },
|
||||
): string | null {
|
||||
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
|
||||
const filePath = candidates.find((p) => fs.existsSync(p));
|
||||
@@ -193,6 +196,9 @@ export function readFirstUserMessageFromTranscript(
|
||||
const parsed = JSON.parse(line);
|
||||
const msg = parsed?.message as TranscriptMessage | undefined;
|
||||
if (msg?.role === "user") {
|
||||
if (opts?.includeInterSession !== true && hasInterSessionUserProvenance(msg)) {
|
||||
continue;
|
||||
}
|
||||
const text = extractTextFromContent(msg.content);
|
||||
if (text) {
|
||||
return text;
|
||||
|
||||
Reference in New Issue
Block a user