惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
罗磊的独立博客
S
SegmentFault 最新的问题
V
V2EX
V
Visual Studio Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
Y
Y Combinator Blog
MyScale Blog
MyScale Blog
D
Docker
Google DeepMind News
Google DeepMind News
Blog — PlanetScale
Blog — PlanetScale
M
Microsoft Research Blog - Microsoft Research
Martin Fowler
Martin Fowler
S
Secure Thoughts
B
Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Recent Announcements
Recent Announcements
MongoDB | Blog
MongoDB | Blog
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
T
True Tiger Recordings
GbyAI
GbyAI
P
Proofpoint News Feed
P
Privacy International News Feed
Jina AI
Jina AI
The Cloudflare Blog
I
Intezer
AWS News Blog
AWS News Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Security Archives - TechRepublic
NISL@THU
NISL@THU
The Register - Security
The Register - Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Palo Alto Networks Blog
S
Schneier on Security
L
LINUX DO - 热门话题
C
CXSECURITY Database RSS Feed - CXSecurity.com
Security Latest
Security Latest
C
Cybersecurity and Infrastructure Security Agency CISA

Recent Commits to openclaw:main

暂无文章

test(qa-lab): add harness sentinel scenarios · openclaw/openclaw@2547e35
vincentkoc · 2026-05-17 · via Recent Commits to openclaw:main

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";

22

import path from "node:path";

33

import { setTimeout as sleep } from "node:timers/promises";

44

import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";

5+

import { scanDirectReplyTranscriptSentinels } from "./gateway-log-sentinel.js";

56

import { liveTurnTimeoutMs } from "./suite-runtime-agent-common.js";

67

import type {

78

QaRawSessionStoreEntry,

@@ -16,6 +17,11 @@ type QaGatewayCallEnv = Pick<

16171718

const SESSION_STORE_LOCK_RETRY_DELAYS_MS = [1_000, 3_000, 5_000] as const;

181920+

type QaSessionTranscriptSummary = {

21+

finalText: string;

22+

hasDirectReplySelfMessage: boolean;

23+

};

24+1925

function isSessionStoreLockTimeout(error: unknown) {

2026

const text = formatErrorMessage(error);

2127

return (

@@ -25,6 +31,73 @@ function isSessionStoreLockTimeout(error: unknown) {

2531

);

2632

}

273334+

function isRecord(value: unknown): value is Record<string, unknown> {

35+

return Boolean(value) && typeof value === "object" && !Array.isArray(value);

36+

}

37+38+

function readNonEmptyString(value: unknown): string | undefined {

39+

return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;

40+

}

41+42+

function extractSessionTranscriptText(message: Record<string, unknown>) {

43+

const rawContent = message.content;

44+

if (typeof rawContent === "string") {

45+

return rawContent.trim();

46+

}

47+

if (!Array.isArray(rawContent)) {

48+

return "";

49+

}

50+

const parts: string[] = [];

51+

for (const block of rawContent) {

52+

if (typeof block === "string") {

53+

if (block.trim()) {

54+

parts.push(block.trim());

55+

}

56+

continue;

57+

}

58+

if (!isRecord(block)) {

59+

continue;

60+

}

61+

const text = readNonEmptyString(block.text);

62+

if (text) {

63+

parts.push(text);

64+

continue;

65+

}

66+

const content = readNonEmptyString(block.content);

67+

if (

68+

content &&

69+

(block.type === "output_text" || block.type === "text" || block.type === "message")

70+

) {

71+

parts.push(content);

72+

}

73+

}

74+

return parts.join("\n").trim();

75+

}

76+77+

function extractFinalAssistantTextFromTranscript(transcriptBytes: string) {

78+

let finalText = "";

79+

for (const line of transcriptBytes.split(/\r?\n/u)) {

80+

const trimmed = line.trim();

81+

if (!trimmed) {

82+

continue;

83+

}

84+

try {

85+

const parsed = JSON.parse(trimmed) as unknown;

86+

const message = isRecord(parsed) && isRecord(parsed.message) ? parsed.message : undefined;

87+

if (!message || message.role !== "assistant") {

88+

continue;

89+

}

90+

const text = extractSessionTranscriptText(message);

91+

if (text) {

92+

finalText = text;

93+

}

94+

} catch {

95+

// Ignore malformed transcript rows and keep QA summary checks deterministic.

96+

}

97+

}

98+

return finalText;

99+

}

100+28101

async function callGatewayWithSessionStoreLockRetry<T>(

29102

env: QaGatewayCallEnv,

30103

method: string,

@@ -106,6 +179,18 @@ async function readSkillStatus(env: QaGatewayCallEnv, agentId = "qa") {

106179

return payload.skills ?? [];

107180

}

108181182+

function resolveQaSessionTranscriptFile(params: {

183+

sessionsDir: string;

184+

sessionId: string;

185+

sessionFile?: string;

186+

}) {

187+

const explicit = readNonEmptyString(params.sessionFile);

188+

if (explicit) {

189+

return path.isAbsolute(explicit) ? explicit : path.join(params.sessionsDir, explicit);

190+

}

191+

return path.join(params.sessionsDir, `${params.sessionId}.jsonl`);

192+

}

193+109194

async function readRawQaSessionStore(env: Pick<QaSuiteRuntimeEnv, "gateway">) {

110195

const storePath = path.join(

111196

env.gateway.tempRoot,

@@ -126,4 +211,40 @@ async function readRawQaSessionStore(env: Pick<QaSuiteRuntimeEnv, "gateway">) {

126211

}

127212

}

128213129-

export { createSession, readEffectiveTools, readRawQaSessionStore, readSkillStatus };

214+

async function readSessionTranscriptSummary(

215+

env: Pick<QaSuiteRuntimeEnv, "gateway">,

216+

sessionKey: string,

217+

): Promise<QaSessionTranscriptSummary> {

218+

const normalizedSessionKey = sessionKey.trim();

219+

if (!normalizedSessionKey) {

220+

throw new Error("readSessionTranscriptSummary requires a session key");

221+

}

222+

const store = await readRawQaSessionStore(env);

223+

const entry = store[normalizedSessionKey];

224+

const sessionId = readNonEmptyString(entry?.sessionId);

225+

if (!sessionId) {

226+

throw new Error(`session transcript entry not found for ${normalizedSessionKey}`);

227+

}

228+

const sessionsDir = path.join(env.gateway.tempRoot, "state", "agents", "qa", "sessions");

229+

const transcriptPath = resolveQaSessionTranscriptFile({

230+

sessionsDir,

231+

sessionId,

232+

sessionFile: entry?.sessionFile,

233+

});

234+

const transcriptBytes = await fs.readFile(transcriptPath, "utf8");

235+

if (!transcriptBytes.trim()) {

236+

throw new Error(`session transcript is empty for ${normalizedSessionKey}`);

237+

}

238+

return {

239+

finalText: extractFinalAssistantTextFromTranscript(transcriptBytes),

240+

hasDirectReplySelfMessage: scanDirectReplyTranscriptSentinels(transcriptBytes).length > 0,

241+

};

242+

}

243+244+

export {

245+

createSession,

246+

readEffectiveTools,

247+

readRawQaSessionStore,

248+

readSessionTranscriptSummary,

249+

readSkillStatus,

250+

};