@tea-agent/loop-agent 0.33.3 → 0.33.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/dist/executors/dag-pi-executor.js +34 -12
- package/dist/executors/shell-executor.js +26 -66
- package/dist/task/config-types.js +2 -2
- package/dist/worker/console/chat/chat-event-store.js +57 -18
- package/dist/worker/console/chat/routes.js +850 -170
- package/dist/worker/console/static/assets/{index-PzYzcuFG.js → index-CteJFFL2.js} +17 -17
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +80 -2
- package/dist/workflows/dag/backend-test-markdown-workflow.js +17 -0
- package/dist/workflows/dag/backend-test-module-stem.js +21 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +44 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +39 -31
- package/dist/workflows/dag/backend-test-writer-completeness.js +130 -45
- package/dist/workflows/dag/init-hybrid.js +29 -160
- package/docs/templates/backend-test-dag.json +3 -3
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +2 -2
- package/docs/templates/frontend-test-dag.json +9 -9
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -7
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/playwright-cli/SKILL.md +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.33.5] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 全面加固 Operator Chat 流式响应的执行可靠性,对齐 pi-web 四层保障机制,彻底解决重连后界面无限加载、空占位气泡卡死等问题
|
|
10
|
+
- 优化前端测试的目标 URL 解析机制,改为从运行时上下文精准解析,并丰富执行报告的细节展示
|
|
11
|
+
- 修复测试生成与执行环境中的多处转义错误,提升复杂脚本生成的稳定性
|
|
12
|
+
|
|
13
|
+
### 新增
|
|
14
|
+
|
|
15
|
+
- 新增轻量级会话状态轮询接口(GET /sessions/:id/state),用于快速获取真实的活跃任务状态
|
|
16
|
+
- 前端测试执行报告新增更丰富的细节展示,并支持从运行时上下文中精准解析目标 URL
|
|
17
|
+
|
|
18
|
+
### 改进
|
|
19
|
+
|
|
20
|
+
- Operator Chat 客户端现支持每 15 秒及页面可见性变化时定期对账服务端真实状态,当服务端已空闲而客户端仍在加载时自动强制结束
|
|
21
|
+
- 优化重连时的回放保护机制:仅当回放事件的任务 ID 与服务端真实活跃 ID 匹配时才继续流式响应,防止已结束的任务被错误唤醒
|
|
22
|
+
- 前端测试默认短链失败用例现仅重跑 1 次,而阻断性审查节点分支默认重跑 2 次,显式配置仍可覆盖默认值
|
|
23
|
+
|
|
24
|
+
### 修复
|
|
25
|
+
|
|
26
|
+
- 修复 Operator Chat 在中断或失败时未写入持久结束事件,导致重连回放卡在开始阶段、界面无限加载的问题
|
|
27
|
+
- 修复对话中断或结束时残留空白占位气泡和卡死省略号的问题,现自动清理未产生内容的消息
|
|
28
|
+
- 修复前端测试重跑选择器生成的 Shell JavaScript 丢失正则反斜杠和换行转义,导致报语法错误的问题
|
|
29
|
+
- 修复前端测试环境预检脚本将 URL 尾部反引号匹配规则展开为裸 Shell 反引号,导致命令替换未闭合的问题
|
|
30
|
+
- 修复后端测试偶尔按优先级生成单一模块文件的问题,现对非法模块命名执行严格阻断
|
|
31
|
+
- 修复后端测试分片完整性校验误判合法测试字符串中未配对分隔符为脚本截断的问题
|
|
32
|
+
|
|
33
|
+
## [0.33.4] - 2026-08-10
|
|
34
|
+
|
|
35
|
+
### 重点更新
|
|
36
|
+
|
|
37
|
+
- 修复后端测试场景参数评估器对 fallback 字段的误判问题,解除由此导致的错误执行阻断
|
|
38
|
+
|
|
39
|
+
### 修复
|
|
40
|
+
|
|
41
|
+
- 修复 backend-test 场景参数评估器在 Markdown 无可解析场景意图时,将 TP-ID 推断的 fallback 字段(如 `TP-CREATE-APPROVER-FORMAT` → `cREATE`/`pattern-invalid`)被大写启发式误判为不匹配的问题;此类条目现统一判定为无法判定且不可修复,不再生成无效修复建议,由此导致的错误执行就绪阻断已解除
|
|
42
|
+
- 修复 fallback 场景参数状态被错误标记为失败的问题,现根据实际情况正确标记为不可用或部分可用,且不弱化任何就绪性门禁
|
|
43
|
+
|
|
5
44
|
## [0.33.3] - 2026-08-10
|
|
6
45
|
|
|
7
46
|
### 重点更新
|
|
@@ -324,25 +324,47 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
|
|
|
324
324
|
await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
|
|
325
325
|
return { promptPath, summaryPath };
|
|
326
326
|
}
|
|
327
|
-
async function
|
|
328
|
-
const capabilityPath = path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json");
|
|
329
|
-
let capability;
|
|
327
|
+
async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
|
|
330
328
|
try {
|
|
331
|
-
capability = JSON.parse(await readFile(
|
|
329
|
+
const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
|
|
330
|
+
if (capability.status !== "ready" ||
|
|
331
|
+
capability.structuredToolName !== "playwright_cli") {
|
|
332
|
+
throw new Error("invalid capability artifact");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
throw new Error("browser-command-capability-unavailable: missing or invalid frontend browser capability artifact");
|
|
337
|
+
}
|
|
338
|
+
const contextPath = path.join(workspaceRoot, "testcase", "frontend", "rag", "context.md");
|
|
339
|
+
let markdown;
|
|
340
|
+
try {
|
|
341
|
+
markdown = await readFile(contextPath, "utf8");
|
|
332
342
|
}
|
|
333
343
|
catch {
|
|
334
|
-
throw new Error("browser-command-capability-unavailable: missing
|
|
344
|
+
throw new Error("browser-command-capability-unavailable: missing frontend context.md after environment preflight");
|
|
345
|
+
}
|
|
346
|
+
const patterns = [
|
|
347
|
+
/baseUrl\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
348
|
+
/base[-_ ]url\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
349
|
+
/playwright-cli open --browser=chrome\s+((?:https?):\/\/[^\s"'`<>]+)/i,
|
|
350
|
+
];
|
|
351
|
+
let rawBaseUrl;
|
|
352
|
+
for (const pattern of patterns) {
|
|
353
|
+
const match = markdown.match(pattern);
|
|
354
|
+
if (match?.[1]) {
|
|
355
|
+
rawBaseUrl = match[1].replace(/[)\]},.;]+$/, "");
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
335
358
|
}
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
throw new Error("browser-command-capability-unavailable: invalid controller-owned frontend browser capability artifact");
|
|
359
|
+
if (!rawBaseUrl || !/environmentProbe\s*[:=]\s*reachable/i.test(markdown)) {
|
|
360
|
+
throw new Error("browser-command-capability-unavailable: frontend context URL is missing or not environment-validated");
|
|
339
361
|
}
|
|
340
362
|
let parsed;
|
|
341
363
|
try {
|
|
342
|
-
parsed = new URL(
|
|
364
|
+
parsed = new URL(rawBaseUrl);
|
|
343
365
|
}
|
|
344
366
|
catch {
|
|
345
|
-
throw new Error("browser-command-capability-unavailable: invalid
|
|
367
|
+
throw new Error("browser-command-capability-unavailable: invalid frontend context origin");
|
|
346
368
|
}
|
|
347
369
|
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
348
370
|
parsed.username ||
|
|
@@ -350,7 +372,7 @@ async function resolveControllerOwnedFrontendBaseUrl(runDir) {
|
|
|
350
372
|
parsed.search ||
|
|
351
373
|
parsed.hash ||
|
|
352
374
|
/(?:^|\.)(?:www\.)?[^.]*(?:prod|production)/i.test(parsed.hostname)) {
|
|
353
|
-
throw new Error("browser-command-capability-unavailable: unsafe
|
|
375
|
+
throw new Error("browser-command-capability-unavailable: unsafe frontend context origin");
|
|
354
376
|
}
|
|
355
377
|
return parsed.toString();
|
|
356
378
|
}
|
|
@@ -466,7 +488,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
466
488
|
nodeId: input.task.id,
|
|
467
489
|
caseId,
|
|
468
490
|
evidenceDir,
|
|
469
|
-
baseUrl: await
|
|
491
|
+
baseUrl: await resolveValidatedFrontendBaseUrlFromContext(input.cwd, meta.runDir),
|
|
470
492
|
inputRoot: "testcase/frontend/fixtures",
|
|
471
493
|
};
|
|
472
494
|
// Cleanup must be confirmed before a new case; otherwise default-session
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
@@ -30,9 +29,9 @@ import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBa
|
|
|
30
29
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
31
30
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
32
31
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
33
|
-
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
32
|
+
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
34
33
|
import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
|
|
35
|
-
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
34
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
36
35
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
37
36
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
38
37
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -40,7 +39,6 @@ import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcela
|
|
|
40
39
|
import { parseAndValidateFinalWriteSetApproval, } from "../workflows/dag/node-execution.js";
|
|
41
40
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
42
41
|
import { readRunState } from "../workflows/dag/run-store.js";
|
|
43
|
-
import { resolveDagTaskSourcePath } from "../task/dag-source-paths.js";
|
|
44
42
|
import { readProjectGovernanceContext } from "../workflows/dag/project-governance-context.js";
|
|
45
43
|
import { assertMavenPlanFresh, MavenPlanStaleError, } from "../verification/maven/index.js";
|
|
46
44
|
async function runBackendScenarioParamPreflight(input) {
|
|
@@ -197,56 +195,6 @@ export function resolveShellCwd(root, requestedCwd) {
|
|
|
197
195
|
}
|
|
198
196
|
return resolved;
|
|
199
197
|
}
|
|
200
|
-
async function resolveControllerFrontendBaseUrlForPreflight(input) {
|
|
201
|
-
const binding = input.spec.sourceBinding;
|
|
202
|
-
const candidates = (binding?.sources ?? []).filter((source) => /(?:^|\/)config\.md$/i.test(source.path));
|
|
203
|
-
let rawBaseUrl = "http://localhost:5173";
|
|
204
|
-
let baseUrlSource = "default-localhost-5173";
|
|
205
|
-
let baseUrlSourceSha256;
|
|
206
|
-
for (const source of candidates) {
|
|
207
|
-
const absolutePath = resolveDagTaskSourcePath({
|
|
208
|
-
workspaceRoot: input.workspaceRoot,
|
|
209
|
-
taskId: binding.taskId,
|
|
210
|
-
sourcePath: source.path,
|
|
211
|
-
});
|
|
212
|
-
const markdown = await readFile(absolutePath, "utf8");
|
|
213
|
-
const actualSha256 = createHash("sha256").update(markdown).digest("hex");
|
|
214
|
-
if (actualSha256 !== source.sha256) {
|
|
215
|
-
throw new Error(`browser-command-capability-unavailable: stale frontend source ${source.path}`);
|
|
216
|
-
}
|
|
217
|
-
const match = markdown.match(/(?:frontend[_-]?base[_-]?url|base[_-]?url|base[- ]url)\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i);
|
|
218
|
-
if (!match?.[1]) {
|
|
219
|
-
if (/(?:frontend[_-]?base[_-]?url|base[_-]?url|base[- ]url)\s*[:=]/i.test(markdown)) {
|
|
220
|
-
throw new Error(`browser-command-capability-unavailable: invalid controller baseUrl from ${source.path}`);
|
|
221
|
-
}
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
rawBaseUrl = match[1].replace(/[)\]},.;]+$/, "");
|
|
225
|
-
baseUrlSource = source.path;
|
|
226
|
-
baseUrlSourceSha256 = source.sha256;
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
let parsed;
|
|
230
|
-
try {
|
|
231
|
-
parsed = new URL(rawBaseUrl);
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
throw new Error(`browser-command-capability-unavailable: invalid controller baseUrl from ${baseUrlSource}`);
|
|
235
|
-
}
|
|
236
|
-
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
237
|
-
parsed.username ||
|
|
238
|
-
parsed.password ||
|
|
239
|
-
parsed.search ||
|
|
240
|
-
parsed.hash ||
|
|
241
|
-
/(?:^|\.)(?:www\.)?[^.]*(?:prod|production)/i.test(parsed.hostname)) {
|
|
242
|
-
throw new Error(`browser-command-capability-unavailable: unsafe controller baseUrl from ${baseUrlSource}`);
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
baseUrl: parsed.toString(),
|
|
246
|
-
baseUrlSource,
|
|
247
|
-
...(baseUrlSourceSha256 ? { baseUrlSourceSha256 } : {}),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
198
|
async function resolveJsonArtifactSourceNodeId(input) {
|
|
251
199
|
const candidates = [
|
|
252
200
|
input.fromNodeId,
|
|
@@ -598,6 +546,15 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
598
546
|
durationMs: Date.now() - started,
|
|
599
547
|
};
|
|
600
548
|
}
|
|
549
|
+
if (hasBlockingBackendMarkdownModuleStemFindings(report)) {
|
|
550
|
+
return {
|
|
551
|
+
ok: false,
|
|
552
|
+
stdout: outputs.join("\n"),
|
|
553
|
+
stderr: "backend Markdown module stem gate blocked: priority-only-module-stem",
|
|
554
|
+
failureCategory: "invalid-output",
|
|
555
|
+
durationMs: Date.now() - started,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
601
558
|
const sourceBinding = meta.spec.sourceBinding;
|
|
602
559
|
if (!sourceBinding) {
|
|
603
560
|
const coverage = "# Backend Test Case Coverage Analysis\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Coverage analysis requires spec.sourceBinding; node 4 and node 7 fail closed on the same task/source binding contract.\n";
|
|
@@ -651,7 +608,16 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
651
608
|
else if (pipeline === "markdown-collection-assess") {
|
|
652
609
|
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
|
|
653
610
|
let facts;
|
|
654
|
-
|
|
611
|
+
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
612
|
+
const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
|
|
613
|
+
phase: "initial",
|
|
614
|
+
mappedScripts: resolution.expectedScripts,
|
|
615
|
+
inventory: preflightInventory,
|
|
616
|
+
});
|
|
617
|
+
if (priorityOnlyFacts) {
|
|
618
|
+
facts = priorityOnlyFacts;
|
|
619
|
+
}
|
|
620
|
+
else if (resolution.missingScripts.length > 0) {
|
|
655
621
|
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
656
622
|
facts = assessMissingBackendPytestScripts({
|
|
657
623
|
mappedScripts: resolution.expectedScripts,
|
|
@@ -2138,17 +2104,15 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2138
2104
|
if (!sdkCapability.ok) {
|
|
2139
2105
|
throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
|
|
2140
2106
|
}
|
|
2141
|
-
const controllerOrigin = await resolveControllerFrontendBaseUrlForPreflight({
|
|
2142
|
-
workspaceRoot: input.cwd,
|
|
2143
|
-
spec: meta.spec,
|
|
2144
|
-
});
|
|
2145
2107
|
const toolProbe = await createPlaywrightCliTool({
|
|
2146
2108
|
repoRoot: input.cwd,
|
|
2147
2109
|
runDir: meta.runDir,
|
|
2148
2110
|
nodeId: input.task.id,
|
|
2149
2111
|
caseId: "frontend-browser-preflight",
|
|
2150
2112
|
evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
|
|
2151
|
-
|
|
2113
|
+
// Capability-only registration probe. The tested URL is selected later by
|
|
2114
|
+
// retrieve-frontend-test-context-pi and validated by the environment shell.
|
|
2115
|
+
baseUrl: "http://localhost:5173/",
|
|
2152
2116
|
inputRoot: "testcase/frontend/fixtures",
|
|
2153
2117
|
});
|
|
2154
2118
|
if (toolProbe.name !== "playwright_cli") {
|
|
@@ -2165,7 +2129,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2165
2129
|
const help = `${result.stdout}\n${result.stderr}`;
|
|
2166
2130
|
if (result.exitCode !== 0 ||
|
|
2167
2131
|
result.timedOut ||
|
|
2168
|
-
!["open", "close", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
|
|
2132
|
+
!["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
|
|
2169
2133
|
throw new Error("playwright-cli-contract-incompatible");
|
|
2170
2134
|
}
|
|
2171
2135
|
const capability = {
|
|
@@ -2175,11 +2139,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2175
2139
|
launcher: "verified-js-entry",
|
|
2176
2140
|
sdkCustomToolCapability: true,
|
|
2177
2141
|
structuredToolName: toolProbe.name,
|
|
2178
|
-
|
|
2179
|
-
baseUrlSource: controllerOrigin.baseUrlSource,
|
|
2180
|
-
...(controllerOrigin.baseUrlSourceSha256
|
|
2181
|
-
? { baseUrlSourceSha256: controllerOrigin.baseUrlSourceSha256 }
|
|
2182
|
-
: {}),
|
|
2142
|
+
targetUrlAuthority: "retrieve-context-and-environment-probe",
|
|
2183
2143
|
};
|
|
2184
2144
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
|
|
2185
2145
|
return {
|
|
@@ -85,8 +85,8 @@ export const frontendTestConfigSchema = z.object({
|
|
|
85
85
|
strictOutcomeGate: z.boolean().optional(),
|
|
86
86
|
/**
|
|
87
87
|
* Maximum rerun attempts for blocked or missing-result frontend cases.
|
|
88
|
-
* Defaults to
|
|
89
|
-
*
|
|
88
|
+
* Defaults to 1 for the lean off/advisory topology and 2 for the full
|
|
89
|
+
* reviewMode=blocking topology. An explicit value overrides either default.
|
|
90
90
|
*/
|
|
91
91
|
maxRerunAttempts: z.number().int().min(0).max(4).optional(),
|
|
92
92
|
/**
|
|
@@ -41,7 +41,8 @@ function controllerSummary(operation) {
|
|
|
41
41
|
const record = identity;
|
|
42
42
|
const fingerprint = typeof record.fingerprint === "string"
|
|
43
43
|
? record.fingerprint
|
|
44
|
-
: typeof record.packageFingerprint
|
|
44
|
+
: typeof record.packageFingerprint
|
|
45
|
+
?.value === "string"
|
|
45
46
|
? String(record.packageFingerprint.value)
|
|
46
47
|
: undefined;
|
|
47
48
|
const label = [record.packageName, record.packageVersion, fingerprint]
|
|
@@ -61,7 +62,12 @@ export function projectOperationForChat(operation) {
|
|
|
61
62
|
const workerRunId = safeIdentity(params.workerRunId);
|
|
62
63
|
const controller = controllerSummary(operation);
|
|
63
64
|
const previewHash = createHash("sha256")
|
|
64
|
-
.update(JSON.stringify({
|
|
65
|
+
.update(JSON.stringify({
|
|
66
|
+
stdoutPreview,
|
|
67
|
+
stderrPreview,
|
|
68
|
+
summaryPreview,
|
|
69
|
+
state: operation.state,
|
|
70
|
+
}))
|
|
65
71
|
.digest("hex");
|
|
66
72
|
return {
|
|
67
73
|
schemaVersion: 1,
|
|
@@ -79,25 +85,34 @@ export function projectOperationForChat(operation) {
|
|
|
79
85
|
...(stdoutPreview ? { stdoutPreview } : {}),
|
|
80
86
|
...(stderrPreview ? { stderrPreview } : {}),
|
|
81
87
|
...(summaryPreview ? { summaryPreview } : {}),
|
|
82
|
-
...(operation.errorCode
|
|
88
|
+
...(operation.errorCode
|
|
89
|
+
? { errorCode: safeIdentity(operation.errorCode) }
|
|
90
|
+
: {}),
|
|
83
91
|
previewHash,
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
export function projectCompactSnapshot(value) {
|
|
87
|
-
const record = value && typeof value === "object"
|
|
95
|
+
const record = value && typeof value === "object"
|
|
96
|
+
? value
|
|
97
|
+
: {};
|
|
88
98
|
const summary = boundedRedactedText(record.summary, 12_000) ?? "Context compacted.";
|
|
89
99
|
const firstKeptEntryId = safeIdentity(record.firstKeptEntryId);
|
|
90
|
-
const tokensBefore = typeof record.tokensBefore === "number" &&
|
|
100
|
+
const tokensBefore = typeof record.tokensBefore === "number" &&
|
|
101
|
+
Number.isFinite(record.tokensBefore)
|
|
91
102
|
? Math.max(0, Math.floor(record.tokensBefore))
|
|
92
103
|
: undefined;
|
|
93
|
-
const details = record.details === undefined
|
|
104
|
+
const details = record.details === undefined
|
|
105
|
+
? undefined
|
|
106
|
+
: boundedRedactedJson(record.details).preview;
|
|
94
107
|
return {
|
|
95
108
|
schemaVersion: 1,
|
|
96
109
|
summary,
|
|
97
110
|
...(firstKeptEntryId ? { firstKeptEntryId } : {}),
|
|
98
111
|
...(tokensBefore !== undefined ? { tokensBefore } : {}),
|
|
99
112
|
...(details ? { details } : {}),
|
|
100
|
-
previewHash: createHash("sha256")
|
|
113
|
+
previewHash: createHash("sha256")
|
|
114
|
+
.update(JSON.stringify({ summary, firstKeptEntryId, tokensBefore, details }))
|
|
115
|
+
.digest("hex"),
|
|
101
116
|
};
|
|
102
117
|
}
|
|
103
118
|
function boundedRedactedJson(value) {
|
|
@@ -145,7 +160,12 @@ export function projectOperationEventSummary(event) {
|
|
|
145
160
|
...(messagePreview ? { messagePreview } : {}),
|
|
146
161
|
...(dataPreview ? { dataPreview } : {}),
|
|
147
162
|
previewHash: createHash("sha256")
|
|
148
|
-
.update(JSON.stringify({
|
|
163
|
+
.update(JSON.stringify({
|
|
164
|
+
messagePreview,
|
|
165
|
+
dataPreview,
|
|
166
|
+
state: event.state,
|
|
167
|
+
kind: event.kind,
|
|
168
|
+
}))
|
|
149
169
|
.digest("hex"),
|
|
150
170
|
};
|
|
151
171
|
}
|
|
@@ -166,16 +186,17 @@ export function createChatOperationLinker(options) {
|
|
|
166
186
|
if (event.kind !== "operation-ref")
|
|
167
187
|
return false;
|
|
168
188
|
const data = event.data;
|
|
169
|
-
return event.turnId === input.turnId &&
|
|
189
|
+
return (event.turnId === input.turnId &&
|
|
170
190
|
data.operationId === input.operationId &&
|
|
171
|
-
data.toolCallId === input.toolCallId;
|
|
191
|
+
data.toolCallId === input.toolCallId);
|
|
172
192
|
});
|
|
173
193
|
}
|
|
174
194
|
async function syncFact(input) {
|
|
175
195
|
const operation = await options.operations.get(input.operationId);
|
|
176
196
|
if (!operation)
|
|
177
197
|
return;
|
|
178
|
-
const focused = isTerminalOperationState(operation.state) ||
|
|
198
|
+
const focused = isTerminalOperationState(operation.state) ||
|
|
199
|
+
operation.state === "needs-reconcile";
|
|
179
200
|
if (!focused)
|
|
180
201
|
return;
|
|
181
202
|
const projection = projectOperationForChat(operation);
|
|
@@ -184,14 +205,16 @@ export function createChatOperationLinker(options) {
|
|
|
184
205
|
: operation.state === "failed" || operation.state === "timed-out"
|
|
185
206
|
? "failed"
|
|
186
207
|
: "terminal";
|
|
187
|
-
const duplicate = options.chatEvents
|
|
208
|
+
const duplicate = options.chatEvents
|
|
209
|
+
.snapshot(input.sessionId)
|
|
210
|
+
.some((event) => {
|
|
188
211
|
if (event.kind !== "operation-status")
|
|
189
212
|
return false;
|
|
190
213
|
const data = event.data;
|
|
191
|
-
return data.operationId === input.operationId &&
|
|
214
|
+
return (data.operationId === input.operationId &&
|
|
192
215
|
data.closureKind === closureKind &&
|
|
193
216
|
data.operation?.state === projection.state &&
|
|
194
|
-
data.operation?.previewHash === projection.previewHash;
|
|
217
|
+
data.operation?.previewHash === projection.previewHash);
|
|
195
218
|
});
|
|
196
219
|
if (!duplicate) {
|
|
197
220
|
options.chatEvents.append(input.sessionId, input.turnId, {
|
|
@@ -237,7 +260,9 @@ export function createChatOperationLinker(options) {
|
|
|
237
260
|
return {
|
|
238
261
|
link,
|
|
239
262
|
async recoverSession(sessionId) {
|
|
240
|
-
const refs = options.chatEvents
|
|
263
|
+
const refs = options.chatEvents
|
|
264
|
+
.snapshot(sessionId)
|
|
265
|
+
.filter((event) => event.kind === "operation-ref");
|
|
241
266
|
for (const event of refs) {
|
|
242
267
|
const data = event.data;
|
|
243
268
|
if (!data.operationId || !data.toolCallId)
|
|
@@ -339,8 +364,14 @@ export function createChatEventStore(options) {
|
|
|
339
364
|
return {
|
|
340
365
|
forkContextRefs(input) {
|
|
341
366
|
const allowed = new Set([
|
|
342
|
-
"operation-ref",
|
|
343
|
-
"
|
|
367
|
+
"operation-ref",
|
|
368
|
+
"operation-status",
|
|
369
|
+
"draft",
|
|
370
|
+
"assessment",
|
|
371
|
+
"taskKind-confirmed",
|
|
372
|
+
"human-gate-card",
|
|
373
|
+
"compact",
|
|
374
|
+
"artifact-ref",
|
|
344
375
|
]);
|
|
345
376
|
let copiedCount = 0;
|
|
346
377
|
for (const event of this.snapshot(input.sourceSessionId)) {
|
|
@@ -351,7 +382,10 @@ export function createChatEventStore(options) {
|
|
|
351
382
|
delete data.humanGateToken;
|
|
352
383
|
delete data.dagBytesPath;
|
|
353
384
|
}
|
|
354
|
-
this.append(input.targetSessionId, input.targetTurnId, {
|
|
385
|
+
this.append(input.targetSessionId, input.targetTurnId, {
|
|
386
|
+
kind: event.kind,
|
|
387
|
+
data: data,
|
|
388
|
+
});
|
|
355
389
|
copiedCount += 1;
|
|
356
390
|
}
|
|
357
391
|
return { copiedCount };
|
|
@@ -402,6 +436,11 @@ export function createChatEventStore(options) {
|
|
|
402
436
|
const turn = ring.turns.find((candidate) => candidate.state === "queued" || candidate.state === "running");
|
|
403
437
|
return turn ? { ...turn } : undefined;
|
|
404
438
|
},
|
|
439
|
+
latestTurn(sessionId) {
|
|
440
|
+
const ring = ringFor(sessionId);
|
|
441
|
+
const turn = ring.turns.at(-1);
|
|
442
|
+
return turn ? { ...turn } : undefined;
|
|
443
|
+
},
|
|
405
444
|
append(sessionId, turnId, partial) {
|
|
406
445
|
const ring = ringFor(sessionId);
|
|
407
446
|
const seq = ring.nextSeq++;
|