@tea-agent/loop-agent 0.39.0-next.28 → 0.39.0-next.29
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 +7 -0
- package/dist/build-stamp.json +2 -2
- package/dist/executors/pi-playwright-cli-tool.js +8 -1
- package/dist/executors/shell-executor.js +159 -7
- package/dist/worker/observe/node-input.js +2 -0
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +24 -0
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +8 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +11 -0
- package/dist/worker/observe/static/styles.css +3 -0
- package/dist/worker/observe/static/views/dag-inspector.js +247 -9
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/failure-category.js +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
### 重点更新
|
|
6
6
|
|
|
7
|
+
- frontend-test `passed` 回执链改按 dispatch 序号判序(B-HASH-01):`summarizePlaywrightCliReceipts` 先按 `sequence` 排序再判 open→find→cleanup 连续链;并行 `find`/`screenshot` 按完成时间落盘导致的乱序不再误杀执行已通过的 case(Wave 1 实测 4 条 passed 被改判 `browser-command-evidence-missing`)。真 gap / 重复序号仍 fail-closed。
|
|
8
|
+
- `tool-policy` 失败归一为 `executor`(B-REC-03):前端 preflight 能力拒绝(缺 `@playwright/cli` JS entry、allowlist 拒绝)不再落 `unknown`→`manual-review`+`humanRequired`;`dag report` 给出 `rerun-after-fix`,Console `standaloneTaskRerun` 的 R2 gate 放行 run-failed 同 task 整单重跑,无需人工决策门或 sibling task。
|
|
9
|
+
- Operator Chrome Band1 模式切换入口补显式 `aria-label`(B-FIND-01,方案 A):`操作`/`观测` mode link 原为 inline-flex 裸文本节点,不参与 name-from-content,a11y snapshot 序列化为无名 link,`find` 无法断言;补 `aria-label` 后实测 `find "观测"` 1 命中、click 后 `find "运行总览"` 1 命中,AC-FE-CHROME-003 恢复可见断言路径。
|
|
10
|
+
|
|
7
11
|
- `next-release.yml` 防重复发布守卫补充幂等语义:`mode=recover`(dist-tags.next 已指向出自本 HEAD 的 tarball)时,手动 `workflow_dispatch` 重入不再撞 duplicate publish 硬失败,改为写 summary 后干净退出(§5.1 规则 4);非 recover 的重复发布仍硬失败。
|
|
8
12
|
|
|
9
13
|
- frozen repair 工具审计改为按 attempt 段扫描(B1):`session-events.jsonl` 跨 attempt 共享追加,attempt>1 的 `auditRepairAttemptToolUse` 现在只审本 attempt 字节段(newline 对齐),planner attempt 1 的 ls/find 不再被误判给 repair 轮;repair prompt 显式声明 read-only 工具契约与 allowlist 边界(B2),并内嵌 openspec-citations 输出格式正例;`LOOP_AGENT_IGNORE_STALE_DIST=1` 环境变量旁路 stale-dist guard(B3),Console spawn 的 sibling CLI 可继承。
|
|
@@ -39,6 +43,8 @@
|
|
|
39
43
|
|
|
40
44
|
### 改进
|
|
41
45
|
|
|
46
|
+
- DAG 节点检查器「执行过程」按执行器分流:Pi 仍看 session-events;有命令的 shell 展示 `shell-command-receipt` 与 stdout/stderr;`frontendBrowserToolPreflight` 等内部处理器展示步骤回执(历史 run 可从 capability / attempt 产物合成)。空态不再把 shell/static 误说成「暂无过程日志」。节点输入补充 named handler 名称。脚本仍属于节点输入,不假装已执行。
|
|
47
|
+
- DAG 节点检查器「节点输出」在整段内容为完整 JSON 对象或数组时改为缩进代码块展示,不再把紧凑 `JSON.stringify` 挤成一段 Markdown 段落;截断或非法 JSON、普通 Markdown 仍走原阅读器。
|
|
42
48
|
- frontend DAG 的 materialized source references 恢复统一的有界 excerpt 与按需 read 指针,不再把 requirement/acceptance 文档全文复制进每个相关节点,也不再强制 contract/plan 重读全部绑定源文件,降低长需求在多节点中的重复上下文占用。
|
|
43
49
|
- frontend-test prepare-package 会从 `docs/templates/`、`harness.json` `governanceRoot`/templates 以及 init 投影的 `ai_workspace/loop-agent/templates/` 拷贝标准场景;full init 目标仓不再静默落到仅含 `STD-FE-SMOKE-ENTRY` 的 fallback。
|
|
44
50
|
- frontend-test 环境探测区分 connection-refused(curl 7)与 HTTP 4xx/5xx,并提示先启动本地前端后再从 probe 节点续跑;不再把所有不可达都写成同一句 `frontend-base-url-unreachable`。
|
|
@@ -82,6 +88,7 @@
|
|
|
82
88
|
|
|
83
89
|
### 修复
|
|
84
90
|
|
|
91
|
+
- backend-test 用例覆盖分析的 payload 形状提取在无 `python` 可执行文件的环境(现代 macOS 只有 `python3`)静默降级为空结果,导致 payload 观测路径全空、`PAYLOAD_SHAPE_MISMATCH` 误报;解释器解析改为 POSIX 优先 `python3`、Windows 优先 `python`(与 scenario-param 语法校验一致),仅在解释器缺失时尝试下一候选,分析输入自身的失败不再重试。
|
|
85
92
|
- Inspector「执行过程」时间轴按「执行分次」选择过滤:session-events 是跨 attempt 共享追加的单文件,此前所有次执行的事件重合显示、切换顶部分次不生效;现在按 `agent_start` 边界分段,选中第 N 次执行只显示该段并标注「已按第 N 次执行过滤 · 共 M 次执行」,单次执行与 pass 选择保持全量展示,检测不到边界时显式提示并回退全量。
|
|
86
93
|
- 前端 plan/revision 的 `openspec-citations` fence 提取改为行锚定正则:直接闭合的空 fence(无候选行、紧跟关闭 fence)此前被判定为「不存在 citation 块」而进入格式重试循环,现在计为合法的唯一 citation 块;重复、损坏或非唯一的 fence 仍被拒绝。
|
|
87
94
|
- `npm test` 前置段失败导致后续段(io-heavy / integration)被跳过时,`scripts/run-tests.mjs` 显式输出警告与补跑命令,不再静默跳过(此前失败汇总会被误读为全量覆盖)。
|
package/dist/build-stamp.json
CHANGED
|
@@ -111,6 +111,13 @@ export async function readPlaywrightCliReceipts(runDir, nodeId) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
export function summarizePlaywrightCliReceipts(receipts) {
|
|
114
|
+
// Sequence numbers are assigned at dispatch time, which is the controller's
|
|
115
|
+
// causal order. Receipt lines are appended when each command COMPLETES, so
|
|
116
|
+
// concurrent commands (e.g. parallel find calls issued in one turn) persist
|
|
117
|
+
// out of sequence order. Judge chain contiguity on the dispatch order by
|
|
118
|
+
// sorting on `sequence` first; a genuinely missing receipt (gap) or a
|
|
119
|
+
// duplicated sequence still fails the contiguity check below.
|
|
120
|
+
const orderedReceipts = [...receipts].sort((a, b) => a.sequence - b.sequence);
|
|
114
121
|
let hasSuccessfulOpen = false;
|
|
115
122
|
let hasInteractionOrAssertion = false;
|
|
116
123
|
let hasMeaningfulAssertion = false;
|
|
@@ -122,7 +129,7 @@ export function summarizePlaywrightCliReceipts(receipts) {
|
|
|
122
129
|
let hasContiguousControllerSequence = true;
|
|
123
130
|
let previousSequence = 0;
|
|
124
131
|
const caseIds = new Set();
|
|
125
|
-
for (const receipt of
|
|
132
|
+
for (const receipt of orderedReceipts) {
|
|
126
133
|
caseIds.add(receipt.caseId);
|
|
127
134
|
const succeeded = receipt.exitCode === 0 && !receipt.timedOut;
|
|
128
135
|
const ordered = receipt.sequence === previousSequence + 1;
|
|
@@ -2386,7 +2386,76 @@ async function executeFrontendLintBaseline(input, meta) {
|
|
|
2386
2386
|
};
|
|
2387
2387
|
}
|
|
2388
2388
|
}
|
|
2389
|
-
|
|
2389
|
+
async function writeShellPresetProcessReceipt(input) {
|
|
2390
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
|
|
2391
|
+
schemaVersion: 1,
|
|
2392
|
+
kind: "shell-preset",
|
|
2393
|
+
preset: input.preset,
|
|
2394
|
+
ok: input.ok,
|
|
2395
|
+
durationMs: input.durationMs,
|
|
2396
|
+
steps: input.steps,
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
async function ensureShellPresetProcessReceipt(input) {
|
|
2400
|
+
const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
|
|
2401
|
+
if (existsSync(receiptPath))
|
|
2402
|
+
return;
|
|
2403
|
+
if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const detail = input.result.ok ? input.result.stdout : input.result.stderr;
|
|
2407
|
+
await writeShellPresetProcessReceipt({
|
|
2408
|
+
runDir: input.runDir,
|
|
2409
|
+
nodeId: input.nodeId,
|
|
2410
|
+
preset: input.preset,
|
|
2411
|
+
ok: input.result.ok,
|
|
2412
|
+
durationMs: input.result.durationMs,
|
|
2413
|
+
steps: [
|
|
2414
|
+
{
|
|
2415
|
+
id: "run",
|
|
2416
|
+
label: input.preset,
|
|
2417
|
+
ok: input.result.ok,
|
|
2418
|
+
detail: detail?.slice(0, 8 * 1024),
|
|
2419
|
+
durationMs: input.result.durationMs,
|
|
2420
|
+
},
|
|
2421
|
+
],
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
function namedShellHandler(shell) {
|
|
2425
|
+
if (!shell)
|
|
2426
|
+
return null;
|
|
2427
|
+
const record = shell;
|
|
2428
|
+
for (const key of [
|
|
2429
|
+
"finalWriteSetApprovalGate",
|
|
2430
|
+
"frontendTestStandardScenarios",
|
|
2431
|
+
"frontendTestEnvironmentProbe",
|
|
2432
|
+
"frontendBrowserToolPreflight",
|
|
2433
|
+
"frontendPrewriteGate",
|
|
2434
|
+
"frontendLintBaseline",
|
|
2435
|
+
"frontendVerificationBundle",
|
|
2436
|
+
"frontendReviewContext",
|
|
2437
|
+
"frontendTestCaseChecklist",
|
|
2438
|
+
"frontendTestCaseManifest",
|
|
2439
|
+
"frontendTestEvidenceValidation",
|
|
2440
|
+
"frontendTestResultFinalize",
|
|
2441
|
+
"frontendTestL5Report",
|
|
2442
|
+
"frontendTestHtmlReport",
|
|
2443
|
+
"frontendTestReports",
|
|
2444
|
+
"backendTestPipeline",
|
|
2445
|
+
"jsonArtifactGate",
|
|
2446
|
+
"projectGovernanceGate",
|
|
2447
|
+
"requirementCoverageGate",
|
|
2448
|
+
"verdictGate",
|
|
2449
|
+
"verifyEvidence",
|
|
2450
|
+
"repairArtifactGate",
|
|
2451
|
+
"jacocoCoverage",
|
|
2452
|
+
]) {
|
|
2453
|
+
if (record[key] != null)
|
|
2454
|
+
return key;
|
|
2455
|
+
}
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
async function executeDagShellNodeInner(input, meta) {
|
|
2390
2459
|
const shell = input.task.shell;
|
|
2391
2460
|
if (shell?.finalWriteSetApprovalGate) {
|
|
2392
2461
|
const started = Date.now();
|
|
@@ -2433,28 +2502,81 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2433
2502
|
}
|
|
2434
2503
|
if (shell?.frontendBrowserToolPreflight) {
|
|
2435
2504
|
const started = Date.now();
|
|
2505
|
+
const steps = [];
|
|
2506
|
+
const finish = async (result) => {
|
|
2507
|
+
await writeShellPresetProcessReceipt({
|
|
2508
|
+
runDir: meta.runDir,
|
|
2509
|
+
nodeId: input.task.id,
|
|
2510
|
+
preset: "frontendBrowserToolPreflight",
|
|
2511
|
+
ok: result.ok,
|
|
2512
|
+
durationMs: result.durationMs,
|
|
2513
|
+
steps: steps.length > 0
|
|
2514
|
+
? steps
|
|
2515
|
+
: [
|
|
2516
|
+
{
|
|
2517
|
+
id: "run",
|
|
2518
|
+
label: "浏览器工具能力探测",
|
|
2519
|
+
ok: result.ok,
|
|
2520
|
+
detail: result.ok ? result.stdout : result.stderr,
|
|
2521
|
+
durationMs: result.durationMs,
|
|
2522
|
+
},
|
|
2523
|
+
],
|
|
2524
|
+
});
|
|
2525
|
+
return result;
|
|
2526
|
+
};
|
|
2436
2527
|
try {
|
|
2437
2528
|
if (resolvePiBackend() === "cli-only") {
|
|
2529
|
+
steps.push({
|
|
2530
|
+
id: "pi-backend",
|
|
2531
|
+
label: "Pi SDK 后端可用",
|
|
2532
|
+
ok: false,
|
|
2533
|
+
detail: "CODE_AGENT_PI_BACKEND=cli-only",
|
|
2534
|
+
});
|
|
2438
2535
|
throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
|
|
2439
2536
|
}
|
|
2537
|
+
steps.push({
|
|
2538
|
+
id: "pi-backend",
|
|
2539
|
+
label: "Pi SDK 后端可用",
|
|
2540
|
+
ok: true,
|
|
2541
|
+
});
|
|
2440
2542
|
const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
|
|
2441
2543
|
if (!sdkCapability.ok) {
|
|
2544
|
+
steps.push({
|
|
2545
|
+
id: "sdk-custom-tool",
|
|
2546
|
+
label: "Pi SDK custom-tool 能力",
|
|
2547
|
+
ok: false,
|
|
2548
|
+
detail: sdkCapability.detail,
|
|
2549
|
+
});
|
|
2442
2550
|
throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
|
|
2443
2551
|
}
|
|
2552
|
+
steps.push({
|
|
2553
|
+
id: "sdk-custom-tool",
|
|
2554
|
+
label: "Pi SDK custom-tool 能力",
|
|
2555
|
+
ok: true,
|
|
2556
|
+
});
|
|
2444
2557
|
const toolProbe = await createPlaywrightCliTool({
|
|
2445
2558
|
repoRoot: input.cwd,
|
|
2446
2559
|
runDir: meta.runDir,
|
|
2447
2560
|
nodeId: input.task.id,
|
|
2448
2561
|
caseId: "frontend-browser-preflight",
|
|
2449
2562
|
evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
|
|
2450
|
-
// Capability-only registration probe. The tested URL is selected later by
|
|
2451
|
-
// retrieve-frontend-test-context-pi and validated by the environment shell.
|
|
2452
2563
|
baseUrl: "http://localhost:5173/",
|
|
2453
2564
|
inputRoot: "testcase/frontend/fixtures",
|
|
2454
2565
|
});
|
|
2455
2566
|
if (toolProbe.name !== "playwright_cli") {
|
|
2567
|
+
steps.push({
|
|
2568
|
+
id: "structured-tool",
|
|
2569
|
+
label: "注册 playwright_cli",
|
|
2570
|
+
ok: false,
|
|
2571
|
+
detail: toolProbe.name,
|
|
2572
|
+
});
|
|
2456
2573
|
throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
|
|
2457
2574
|
}
|
|
2575
|
+
steps.push({
|
|
2576
|
+
id: "structured-tool",
|
|
2577
|
+
label: "注册 playwright_cli",
|
|
2578
|
+
ok: true,
|
|
2579
|
+
});
|
|
2458
2580
|
const launcher = resolvePlaywrightCliLauncher(input.cwd);
|
|
2459
2581
|
const result = await defaultPlaywrightCliCommandRunner({
|
|
2460
2582
|
executable: launcher.executable,
|
|
@@ -2467,8 +2589,19 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2467
2589
|
if (result.exitCode !== 0 ||
|
|
2468
2590
|
result.timedOut ||
|
|
2469
2591
|
!["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
|
|
2592
|
+
steps.push({
|
|
2593
|
+
id: "playwright-cli-help",
|
|
2594
|
+
label: "playwright-cli --help 合同",
|
|
2595
|
+
ok: false,
|
|
2596
|
+
detail: help.slice(0, 8 * 1024),
|
|
2597
|
+
});
|
|
2470
2598
|
throw new Error("playwright-cli-contract-incompatible");
|
|
2471
2599
|
}
|
|
2600
|
+
steps.push({
|
|
2601
|
+
id: "playwright-cli-help",
|
|
2602
|
+
label: "playwright-cli --help 合同",
|
|
2603
|
+
ok: true,
|
|
2604
|
+
});
|
|
2472
2605
|
const capability = {
|
|
2473
2606
|
schemaVersion: 1,
|
|
2474
2607
|
status: "ready",
|
|
@@ -2479,22 +2612,28 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2479
2612
|
targetUrlAuthority: "retrieve-context-and-environment-probe",
|
|
2480
2613
|
};
|
|
2481
2614
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
|
|
2482
|
-
|
|
2615
|
+
steps.push({
|
|
2616
|
+
id: "capability-artifact",
|
|
2617
|
+
label: "写出 frontend-browser-capability.json",
|
|
2618
|
+
ok: true,
|
|
2619
|
+
detail: JSON.stringify(capability, null, 2),
|
|
2620
|
+
});
|
|
2621
|
+
return await finish({
|
|
2483
2622
|
ok: true,
|
|
2484
2623
|
stdout: JSON.stringify(capability),
|
|
2485
2624
|
stderr: "",
|
|
2486
2625
|
failureCategory: "success",
|
|
2487
2626
|
durationMs: Date.now() - started,
|
|
2488
|
-
};
|
|
2627
|
+
});
|
|
2489
2628
|
}
|
|
2490
2629
|
catch (error) {
|
|
2491
|
-
return {
|
|
2630
|
+
return await finish({
|
|
2492
2631
|
ok: false,
|
|
2493
2632
|
stdout: "",
|
|
2494
2633
|
stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
2495
2634
|
failureCategory: "tool-policy",
|
|
2496
2635
|
durationMs: Date.now() - started,
|
|
2497
|
-
};
|
|
2636
|
+
});
|
|
2498
2637
|
}
|
|
2499
2638
|
}
|
|
2500
2639
|
if (shell?.frontendPrewriteGate) {
|
|
@@ -3151,3 +3290,16 @@ export async function executeDagShellNode(input, meta) {
|
|
|
3151
3290
|
...{ commandResults },
|
|
3152
3291
|
};
|
|
3153
3292
|
}
|
|
3293
|
+
export async function executeDagShellNode(input, meta) {
|
|
3294
|
+
const handler = namedShellHandler(input.task.shell);
|
|
3295
|
+
const result = await executeDagShellNodeInner(input, meta);
|
|
3296
|
+
if (handler) {
|
|
3297
|
+
await ensureShellPresetProcessReceipt({
|
|
3298
|
+
runDir: meta.runDir,
|
|
3299
|
+
nodeId: input.task.id,
|
|
3300
|
+
preset: handler,
|
|
3301
|
+
result,
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
return result;
|
|
3305
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
4
4
|
import { resolveDagNodeSkills } from "../../workflows/dag/skills.js";
|
|
5
5
|
import { resolveDagNodePromptRedacted, resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
6
|
+
import { namedShellHandlers } from "./shell-handler-keys.js";
|
|
6
7
|
export const NODE_INPUT_SCHEMA_VERSION = 1;
|
|
7
8
|
export const NODE_INPUT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
8
9
|
const SHELL_GATE_KEYS = [
|
|
@@ -217,6 +218,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExt
|
|
|
217
218
|
: null,
|
|
218
219
|
failFast: typeof shellRaw.failFast === "boolean" ? shellRaw.failFast : null,
|
|
219
220
|
gates: shellGateNames(shellRaw),
|
|
221
|
+
handlers: namedShellHandlers(shellRaw),
|
|
220
222
|
}
|
|
221
223
|
: null;
|
|
222
224
|
const staticPreview = staticRaw && typeof staticRaw.resultMarkdown === "string"
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
3
|
+
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
4
|
+
import { parseExecutionOutputKey, } from "./dag-node-execution-output.js";
|
|
5
|
+
import { resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
6
|
+
import { namedShellHandlers } from "./shell-handler-keys.js";
|
|
7
|
+
export const NODE_PROCESS_SCHEMA_VERSION = 1;
|
|
8
|
+
export const NODE_PROCESS_FIELD_MAX_BYTES = 16 * 1024;
|
|
9
|
+
function boundField(value) {
|
|
10
|
+
if (value == null || value === "")
|
|
11
|
+
return { text: null, truncated: false };
|
|
12
|
+
const redacted = redactSecrets(value);
|
|
13
|
+
const truncated = truncateUtf8Preview(redacted, NODE_PROCESS_FIELD_MAX_BYTES);
|
|
14
|
+
return {
|
|
15
|
+
text: truncated,
|
|
16
|
+
truncated: truncated !== redacted,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function asRecord(value) {
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
21
|
+
return null;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function asString(value) {
|
|
25
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
26
|
+
}
|
|
27
|
+
function asNumber(value) {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
29
|
+
}
|
|
30
|
+
function asBoolean(value) {
|
|
31
|
+
return value === true;
|
|
32
|
+
}
|
|
33
|
+
async function readJsonArtifact(repoRoot, dagRunId, segments) {
|
|
34
|
+
const resolved = resolveDagRunArtifact(repoRoot, dagRunId, segments);
|
|
35
|
+
if (!resolved.ok)
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(resolved.result.absolutePath, "utf8");
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return asRecord(parsed);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function readTextArtifact(repoRoot, dagRunId, segments) {
|
|
47
|
+
const resolved = resolveDagRunArtifact(repoRoot, dagRunId, segments);
|
|
48
|
+
if (!resolved.ok)
|
|
49
|
+
return { text: null, truncated: false };
|
|
50
|
+
try {
|
|
51
|
+
const raw = await readFile(resolved.result.absolutePath, "utf8");
|
|
52
|
+
return boundField(raw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { text: null, truncated: false };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function emptyBody(dagRunId, nodeId, kind, emptyReason, extras = {}) {
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: NODE_PROCESS_SCHEMA_VERSION,
|
|
61
|
+
dagRunId,
|
|
62
|
+
nodeId,
|
|
63
|
+
kind,
|
|
64
|
+
executor: extras.executor ?? null,
|
|
65
|
+
handler: extras.handler ?? null,
|
|
66
|
+
emptyReason,
|
|
67
|
+
key: extras.key ?? null,
|
|
68
|
+
commands: extras.commands ?? [],
|
|
69
|
+
steps: extras.steps ?? [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function classifyKind(task) {
|
|
73
|
+
const executor = asString(task.executor);
|
|
74
|
+
const shell = asRecord(task.shell);
|
|
75
|
+
const handlers = namedShellHandlers(shell);
|
|
76
|
+
const handler = handlers[0] ?? null;
|
|
77
|
+
const commands = shell?.commands;
|
|
78
|
+
const commandsDeclared = Array.isArray(commands) &&
|
|
79
|
+
commands.some((item) => typeof item === "string" && item.trim());
|
|
80
|
+
if (executor === "static" || task.static) {
|
|
81
|
+
return { kind: "static", executor: executor ?? "static", handler, commandsDeclared };
|
|
82
|
+
}
|
|
83
|
+
if (executor === "shell" || shell) {
|
|
84
|
+
if (commandsDeclared) {
|
|
85
|
+
return { kind: "shell-commands", executor: executor ?? "shell", handler, commandsDeclared };
|
|
86
|
+
}
|
|
87
|
+
if (handler) {
|
|
88
|
+
return { kind: "shell-preset", executor: executor ?? "shell", handler, commandsDeclared };
|
|
89
|
+
}
|
|
90
|
+
return { kind: "shell-commands", executor: executor ?? "shell", handler, commandsDeclared };
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
kind: "pi-session",
|
|
94
|
+
executor: executor ?? "pi",
|
|
95
|
+
handler,
|
|
96
|
+
commandsDeclared,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function receiptAttemptSegments(nodeId, parsed) {
|
|
100
|
+
if (parsed?.kind === "attempt") {
|
|
101
|
+
return [nodeId, `attempt-${parsed.index}`, "shell-command-receipt.json"];
|
|
102
|
+
}
|
|
103
|
+
return [nodeId, "shell-command-receipt.json"];
|
|
104
|
+
}
|
|
105
|
+
async function loadShellCommands(repoRoot, dagRunId, nodeId, parsed) {
|
|
106
|
+
const preferred = await readJsonArtifact(repoRoot, dagRunId, receiptAttemptSegments(nodeId, parsed));
|
|
107
|
+
const receipt = preferred ??
|
|
108
|
+
(parsed
|
|
109
|
+
? await readJsonArtifact(repoRoot, dagRunId, [
|
|
110
|
+
nodeId,
|
|
111
|
+
"shell-command-receipt.json",
|
|
112
|
+
])
|
|
113
|
+
: null);
|
|
114
|
+
if (!receipt)
|
|
115
|
+
return null;
|
|
116
|
+
const rawCommands = Array.isArray(receipt.commands) ? receipt.commands : [];
|
|
117
|
+
const commands = [];
|
|
118
|
+
for (let i = 0; i < rawCommands.length; i += 1) {
|
|
119
|
+
const entry = asRecord(rawCommands[i]) ?? {};
|
|
120
|
+
const stdout = await readTextArtifact(repoRoot, dagRunId, [
|
|
121
|
+
nodeId,
|
|
122
|
+
"commands",
|
|
123
|
+
`${i + 1}.stdout.txt`,
|
|
124
|
+
]);
|
|
125
|
+
const stderr = await readTextArtifact(repoRoot, dagRunId, [
|
|
126
|
+
nodeId,
|
|
127
|
+
"commands",
|
|
128
|
+
`${i + 1}.stderr.txt`,
|
|
129
|
+
]);
|
|
130
|
+
const commandBound = boundField(asString(entry.command) ?? "");
|
|
131
|
+
commands.push({
|
|
132
|
+
index: i + 1,
|
|
133
|
+
command: commandBound.text ?? "(command)",
|
|
134
|
+
ok: asBoolean(entry.ok),
|
|
135
|
+
exitCode: asNumber(entry.exitCode),
|
|
136
|
+
failureCategory: asString(entry.failureCategory),
|
|
137
|
+
startedAt: asString(entry.startedAt),
|
|
138
|
+
finishedAt: asString(entry.finishedAt),
|
|
139
|
+
durationMs: asNumber(entry.wallDurationMs) ??
|
|
140
|
+
asNumber(entry.executionDurationMs) ??
|
|
141
|
+
null,
|
|
142
|
+
reused: asBoolean(entry.reused),
|
|
143
|
+
stdout: stdout.text,
|
|
144
|
+
stderr: stderr.text,
|
|
145
|
+
truncated: commandBound.truncated || stdout.truncated || stderr.truncated,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return commands;
|
|
149
|
+
}
|
|
150
|
+
function handlerLabel(handler) {
|
|
151
|
+
if (handler === "frontendBrowserToolPreflight")
|
|
152
|
+
return "浏览器工具能力探测";
|
|
153
|
+
if (handler === "frontendTestStandardScenarios")
|
|
154
|
+
return "标准场景物化";
|
|
155
|
+
if (handler === "frontendTestEnvironmentProbe")
|
|
156
|
+
return "前端环境探测";
|
|
157
|
+
if (handler === "frontendTestCaseChecklist")
|
|
158
|
+
return "用例清单校验";
|
|
159
|
+
if (handler === "frontendTestCaseManifest")
|
|
160
|
+
return "用例清单物化";
|
|
161
|
+
if (handler === "frontendTestEvidenceValidation")
|
|
162
|
+
return "证据校验";
|
|
163
|
+
if (handler === "frontendTestResultFinalize")
|
|
164
|
+
return "结果定稿";
|
|
165
|
+
if (handler === "frontendTestReports" || handler === "frontendTestL5Report" || handler === "frontendTestHtmlReport") {
|
|
166
|
+
return "测试报告";
|
|
167
|
+
}
|
|
168
|
+
if (handler === "frontendPrewriteGate")
|
|
169
|
+
return "前端预写门禁";
|
|
170
|
+
if (handler === "finalWriteSetApprovalGate")
|
|
171
|
+
return "writeSet 终态批准";
|
|
172
|
+
return handler;
|
|
173
|
+
}
|
|
174
|
+
async function loadPresetSteps(repoRoot, dagRunId, nodeId, handler, parsed) {
|
|
175
|
+
const written = await readJsonArtifact(repoRoot, dagRunId, [
|
|
176
|
+
nodeId,
|
|
177
|
+
"process-receipt.json",
|
|
178
|
+
]);
|
|
179
|
+
if (written && Array.isArray(written.steps)) {
|
|
180
|
+
return written.steps.flatMap((item, index) => {
|
|
181
|
+
const step = asRecord(item);
|
|
182
|
+
if (!step)
|
|
183
|
+
return [];
|
|
184
|
+
const detail = boundField(asString(step.detail));
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
id: asString(step.id) ?? `step-${index + 1}`,
|
|
188
|
+
label: asString(step.label) ?? handlerLabel(handler ?? "preset"),
|
|
189
|
+
ok: asBoolean(step.ok),
|
|
190
|
+
detail: detail.text,
|
|
191
|
+
durationMs: asNumber(step.durationMs),
|
|
192
|
+
truncated: detail.truncated,
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const capability = await readJsonArtifact(repoRoot, dagRunId, [
|
|
198
|
+
nodeId,
|
|
199
|
+
"frontend-browser-capability.json",
|
|
200
|
+
]);
|
|
201
|
+
if (capability && handler === "frontendBrowserToolPreflight") {
|
|
202
|
+
const detail = boundField(JSON.stringify(capability, null, 2));
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
id: "capability",
|
|
206
|
+
label: handlerLabel(handler),
|
|
207
|
+
ok: asString(capability.status) !== "failed",
|
|
208
|
+
detail: detail.text,
|
|
209
|
+
durationMs: null,
|
|
210
|
+
truncated: detail.truncated,
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
const attemptIndex = parsed?.kind === "attempt" ? parsed.index : 1;
|
|
215
|
+
const attempt = await readJsonArtifact(repoRoot, dagRunId, [
|
|
216
|
+
nodeId,
|
|
217
|
+
`attempt-${attemptIndex}.json`,
|
|
218
|
+
]);
|
|
219
|
+
if (attempt) {
|
|
220
|
+
const ok = asBoolean(attempt.ok);
|
|
221
|
+
const detail = boundField(ok
|
|
222
|
+
? (asString(attempt.stdout) ?? asString(attempt.assistantText))
|
|
223
|
+
: (asString(attempt.stderr) ?? asString(attempt.stdout)));
|
|
224
|
+
return [
|
|
225
|
+
{
|
|
226
|
+
id: "run",
|
|
227
|
+
label: handlerLabel(handler ?? "preset"),
|
|
228
|
+
ok,
|
|
229
|
+
detail: detail.text,
|
|
230
|
+
durationMs: asNumber(attempt.durationMs),
|
|
231
|
+
truncated: detail.truncated,
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
export async function loadDagNodeProcess(repoRoot, dagRunId, nodeId, rawKey) {
|
|
238
|
+
if (!isSafeObservabilityIdentifier(dagRunId) ||
|
|
239
|
+
!isSafeObservabilityIdentifier(nodeId)) {
|
|
240
|
+
return {
|
|
241
|
+
status: 400,
|
|
242
|
+
body: { error: "Invalid dag run or node identifier", reason: "invalid-id" },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
let parsedKey = null;
|
|
246
|
+
if (rawKey != null && String(rawKey).trim() !== "") {
|
|
247
|
+
const parsed = parseExecutionOutputKey(rawKey);
|
|
248
|
+
if (!parsed.ok) {
|
|
249
|
+
return {
|
|
250
|
+
status: 400,
|
|
251
|
+
body: {
|
|
252
|
+
error: "Invalid execution key (expected attempt-N or pass-N)",
|
|
253
|
+
reason: "invalid-key",
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
parsedKey = parsed;
|
|
258
|
+
}
|
|
259
|
+
const runResolved = resolveDagRunJson(repoRoot, dagRunId);
|
|
260
|
+
if (!runResolved.ok) {
|
|
261
|
+
if (runResolved.reason === "invalid-id") {
|
|
262
|
+
return {
|
|
263
|
+
status: 400,
|
|
264
|
+
body: { error: "Invalid dag run or node identifier", reason: "invalid-id" },
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (runResolved.reason === "ambiguous") {
|
|
268
|
+
return {
|
|
269
|
+
status: 409,
|
|
270
|
+
body: { error: "Ambiguous dag run lifecycle", reason: "ambiguous" },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
status: 404,
|
|
275
|
+
body: { error: "DAG run not found", reason: "not-found" },
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
let spec = {};
|
|
279
|
+
try {
|
|
280
|
+
const raw = JSON.parse(await readFile(runResolved.result.absolutePath, "utf8"));
|
|
281
|
+
spec = asRecord(raw) ?? {};
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return {
|
|
285
|
+
status: 404,
|
|
286
|
+
body: { error: "run.json is unreadable", reason: "not-found" },
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const tasks = Array.isArray(spec.tasks) ? spec.tasks : [];
|
|
290
|
+
const task = tasks
|
|
291
|
+
.map((item) => asRecord(item))
|
|
292
|
+
.find((item) => item && asString(item.id) === nodeId);
|
|
293
|
+
if (!task) {
|
|
294
|
+
return {
|
|
295
|
+
status: 200,
|
|
296
|
+
body: emptyBody(dagRunId, nodeId, "none", "not-executed"),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const classified = classifyKind(task);
|
|
300
|
+
const key = parsedKey?.key ?? null;
|
|
301
|
+
if (classified.kind === "pi-session") {
|
|
302
|
+
return {
|
|
303
|
+
status: 200,
|
|
304
|
+
body: emptyBody(dagRunId, nodeId, "pi-session", "pi-session", {
|
|
305
|
+
executor: classified.executor,
|
|
306
|
+
key,
|
|
307
|
+
}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (classified.kind === "static") {
|
|
311
|
+
return {
|
|
312
|
+
status: 200,
|
|
313
|
+
body: emptyBody(dagRunId, nodeId, "static", "static-no-process", {
|
|
314
|
+
executor: classified.executor,
|
|
315
|
+
key,
|
|
316
|
+
}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (classified.kind === "shell-commands" || classified.kind === "shell-preset") {
|
|
320
|
+
const commands = await loadShellCommands(repoRoot, dagRunId, nodeId, parsedKey);
|
|
321
|
+
if (commands && commands.length > 0) {
|
|
322
|
+
return {
|
|
323
|
+
status: 200,
|
|
324
|
+
body: {
|
|
325
|
+
schemaVersion: NODE_PROCESS_SCHEMA_VERSION,
|
|
326
|
+
dagRunId,
|
|
327
|
+
nodeId,
|
|
328
|
+
kind: "shell-commands",
|
|
329
|
+
executor: classified.executor,
|
|
330
|
+
handler: classified.handler,
|
|
331
|
+
emptyReason: null,
|
|
332
|
+
key,
|
|
333
|
+
commands,
|
|
334
|
+
steps: [],
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (classified.handler || classified.kind === "shell-preset") {
|
|
339
|
+
const steps = await loadPresetSteps(repoRoot, dagRunId, nodeId, classified.handler, parsedKey);
|
|
340
|
+
if (steps.length > 0) {
|
|
341
|
+
return {
|
|
342
|
+
status: 200,
|
|
343
|
+
body: {
|
|
344
|
+
schemaVersion: NODE_PROCESS_SCHEMA_VERSION,
|
|
345
|
+
dagRunId,
|
|
346
|
+
nodeId,
|
|
347
|
+
kind: "shell-preset",
|
|
348
|
+
executor: classified.executor,
|
|
349
|
+
handler: classified.handler,
|
|
350
|
+
emptyReason: null,
|
|
351
|
+
key,
|
|
352
|
+
commands: [],
|
|
353
|
+
steps,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
status: 200,
|
|
360
|
+
body: emptyBody(dagRunId, nodeId, classified.kind, classified.kind === "shell-preset"
|
|
361
|
+
? "shell-preset-pending"
|
|
362
|
+
: "shell-receipt-missing", {
|
|
363
|
+
executor: classified.executor,
|
|
364
|
+
handler: classified.handler,
|
|
365
|
+
key,
|
|
366
|
+
}),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
status: 200,
|
|
371
|
+
body: emptyBody(dagRunId, nodeId, "none", "not-executed", {
|
|
372
|
+
executor: classified.executor,
|
|
373
|
+
handler: classified.handler,
|
|
374
|
+
key,
|
|
375
|
+
}),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
@@ -16,6 +16,7 @@ import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview,
|
|
|
16
16
|
import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
|
|
17
17
|
import { buildDagNodeInput } from "./node-input.js";
|
|
18
18
|
import { loadDagNodeExecutionOutput } from "./dag-node-execution-output.js";
|
|
19
|
+
import { loadDagNodeProcess } from "./node-process.js";
|
|
19
20
|
import { buildObserveHealthV1 } from "./health.js";
|
|
20
21
|
import { buildNightJobDetail, buildNightJobsDoctor, buildNightJobsList, buildNightJobsMorning, } from "./night-jobs.js";
|
|
21
22
|
const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
@@ -100,6 +101,11 @@ export const ROUTES = [
|
|
|
100
101
|
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/execution-output$/,
|
|
101
102
|
handler: handleDagNodeExecutionOutput,
|
|
102
103
|
},
|
|
104
|
+
{
|
|
105
|
+
method: "GET",
|
|
106
|
+
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/process$/,
|
|
107
|
+
handler: handleDagNodeProcess,
|
|
108
|
+
},
|
|
103
109
|
{
|
|
104
110
|
method: "GET",
|
|
105
111
|
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/spec-evidence$/,
|
|
@@ -570,6 +576,24 @@ async function handleDagNodeExecutionOutput(_req, res, match, ctx) {
|
|
|
570
576
|
const result = await loadDagNodeExecutionOutput(ctx.repoRoot, dagRunId, nodeId, key);
|
|
571
577
|
sendJson(res, result.status, result.body);
|
|
572
578
|
}
|
|
579
|
+
async function handleDagNodeProcess(_req, res, match, ctx) {
|
|
580
|
+
const dagRunId = match.params.id;
|
|
581
|
+
const nodeId = match.params.sub;
|
|
582
|
+
if (!isSafeObservabilityIdentifier(dagRunId) ||
|
|
583
|
+
!isSafeObservabilityIdentifier(nodeId)) {
|
|
584
|
+
sendJson(res, 400, { error: "Invalid dag run or node identifier" });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (match.query.has("path") || match.query.has("artifactPath")) {
|
|
588
|
+
sendJson(res, 400, {
|
|
589
|
+
error: "Artifact path is not accepted; use optional key=attempt-N|pass-N",
|
|
590
|
+
reason: "path-not-allowed",
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const result = await loadDagNodeProcess(ctx.repoRoot, dagRunId, nodeId, match.query.get("key"));
|
|
595
|
+
sendJson(res, result.status, result.body);
|
|
596
|
+
}
|
|
573
597
|
async function handleDagNodeSessionEvents(_req, res, match, ctx) {
|
|
574
598
|
const dagRunId = match.params.id;
|
|
575
599
|
const nodeId = match.params.sub;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlist of in-process shell handlers (preset/gate flags on task.shell).
|
|
3
|
+
* Observe projects these names; the executor writes process receipts for them.
|
|
4
|
+
*/
|
|
5
|
+
export const SHELL_NAMED_HANDLER_KEYS = [
|
|
6
|
+
"verdictGate",
|
|
7
|
+
"projectGovernanceGate",
|
|
8
|
+
"requirementCoverageGate",
|
|
9
|
+
"jsonArtifactGate",
|
|
10
|
+
"frontendPrewriteGate",
|
|
11
|
+
"frontendLintBaseline",
|
|
12
|
+
"frontendVerificationBundle",
|
|
13
|
+
"frontendReviewContext",
|
|
14
|
+
"frontendTestCaseChecklist",
|
|
15
|
+
"frontendBrowserToolPreflight",
|
|
16
|
+
"frontendTestStandardScenarios",
|
|
17
|
+
"frontendTestEnvironmentProbe",
|
|
18
|
+
"frontendTestEvidenceValidation",
|
|
19
|
+
"finalWriteSetApprovalGate",
|
|
20
|
+
"frontendTestL5Report",
|
|
21
|
+
"frontendTestHtmlReport",
|
|
22
|
+
"frontendTestCaseManifest",
|
|
23
|
+
"frontendTestResultFinalize",
|
|
24
|
+
"frontendTestReports",
|
|
25
|
+
"backendTestPipeline",
|
|
26
|
+
"jacocoCoverage",
|
|
27
|
+
"verifyEvidence",
|
|
28
|
+
"repairArtifactGate",
|
|
29
|
+
];
|
|
30
|
+
export function namedShellHandlers(shell) {
|
|
31
|
+
if (!shell)
|
|
32
|
+
return [];
|
|
33
|
+
return SHELL_NAMED_HANDLER_KEYS.filter((key) => shell[key] != null);
|
|
34
|
+
}
|
|
@@ -77,6 +77,14 @@ export async function fetchDagNodeInput(runId, nodeId) {
|
|
|
77
77
|
return fetchJsonResult(url);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export async function fetchDagNodeProcess(runId, nodeId, key) {
|
|
81
|
+
const params = new URLSearchParams();
|
|
82
|
+
if (key) params.set("key", String(key));
|
|
83
|
+
const qs = params.toString();
|
|
84
|
+
const url = `/api/dag-runs/${encodeURIComponent(runId)}/nodes/${encodeURIComponent(nodeId)}/process${qs ? `?${qs}` : ""}`;
|
|
85
|
+
return fetchJsonResult(url);
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
/**
|
|
81
89
|
* Fetch a spec-evidence file preview (binding source or successful read).
|
|
82
90
|
* Resolves to the parsed JSON body, or throws an Error carrying the HTTP
|
|
@@ -53,7 +53,14 @@ export const UI_TEXT = {
|
|
|
53
53
|
inputError: "加载节点输入失败。",
|
|
54
54
|
inputFrozenLabel: "冻结 DAG 节点定义",
|
|
55
55
|
inputFingerprintNote: "仅保存 assembled prompt 指纹,全文未持久化",
|
|
56
|
-
noSessionEvents: "
|
|
56
|
+
noSessionEvents: "该 Pi 节点暂无过程日志(可能尚未执行、走 CLI 路径,或为历史 run)",
|
|
57
|
+
noShellProcess: "该 shell 节点尚未留下命令回执。",
|
|
58
|
+
noPresetProcess: "该内部处理器尚未留下过程回执。",
|
|
59
|
+
staticNoProcess: "静态节点没有逐步执行过程,请查看节点输出。",
|
|
60
|
+
processLoading: "正在加载执行过程…",
|
|
61
|
+
processError: "加载执行过程失败。",
|
|
62
|
+
shellCommandsHeading: "Shell 命令",
|
|
63
|
+
shellPresetHeading: "内部处理器",
|
|
57
64
|
noArtifacts: "暂无产物记录。",
|
|
58
65
|
noActiveCommand: "当前没有正在执行的命令。",
|
|
59
66
|
noFailures: "当前没有异常 Task。",
|
|
@@ -345,3 +345,26 @@ export function formatRepoPathDisplay(repoRoot) {
|
|
|
345
345
|
.replace(/^\/Users\/[^/]+/, "~")
|
|
346
346
|
.replace(/^\/home\/[^/]+/, "~");
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* If `text` is a complete JSON object or array, return pretty-printed JSON.
|
|
351
|
+
* Primitives, JSONL, truncated/invalid JSON, and markdown return null so
|
|
352
|
+
* callers can fall back to Markdown rendering.
|
|
353
|
+
*/
|
|
354
|
+
export function prettyJsonBlock(text) {
|
|
355
|
+
if (typeof text !== "string") return null;
|
|
356
|
+
const trimmed = text.trim();
|
|
357
|
+
if (trimmed.length < 2) return null;
|
|
358
|
+
const start = trimmed[0];
|
|
359
|
+
const end = trimmed[trimmed.length - 1];
|
|
360
|
+
if (!((start === "{" && end === "}") || (start === "[" && end === "]"))) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const value = JSON.parse(trimmed);
|
|
365
|
+
if (value === null || typeof value !== "object") return null;
|
|
366
|
+
return JSON.stringify(value, null, 2);
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** Shared safe Markdown → DOM rendering for Observe UI. */
|
|
2
2
|
import { el } from "./dom.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseMarkdownBlocks,
|
|
5
|
+
markdownLinkHref,
|
|
6
|
+
prettyJsonBlock,
|
|
7
|
+
} from "./format.js";
|
|
4
8
|
|
|
5
9
|
export function appendInlineMarkdown(target, text) {
|
|
6
10
|
const tokenPattern = /(`[^`]*`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g;
|
|
@@ -49,6 +53,21 @@ export function appendParagraphMarkdown(target, text) {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
export function renderInspectorDocument(content) {
|
|
57
|
+
const pretty = prettyJsonBlock(content);
|
|
58
|
+
if (pretty != null) {
|
|
59
|
+
const article = el("article", "markdown-output json-output");
|
|
60
|
+
const pre = document.createElement("pre");
|
|
61
|
+
const code = document.createElement("code");
|
|
62
|
+
code.dataset.language = "json";
|
|
63
|
+
code.textContent = pretty;
|
|
64
|
+
pre.appendChild(code);
|
|
65
|
+
article.appendChild(pre);
|
|
66
|
+
return article;
|
|
67
|
+
}
|
|
68
|
+
return renderMarkdown(content);
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
export function renderMarkdown(content) {
|
|
53
72
|
const article = el("article", "markdown-output");
|
|
54
73
|
for (const block of parseMarkdownBlocks(content)) {
|
|
@@ -436,6 +436,10 @@ export function mountOperatorChrome(root, config = {}) {
|
|
|
436
436
|
const a = el("a", `och-mode-btn${btn.active ? " och-on" : ""}`);
|
|
437
437
|
a.href = btn.href;
|
|
438
438
|
a.dataset.mode = btn.id;
|
|
439
|
+
// 2026-08-22: inline-flex 布局下裸文本节点不参与 name-from-content,
|
|
440
|
+
// a11y snapshot 序列化为无名 link;显式 aria-label 保证「操作/观测」
|
|
441
|
+
// 对 find/读屏稳定可断言(frontend-test passed 权威依赖可见 find)。
|
|
442
|
+
a.setAttribute("aria-label", btn.label);
|
|
439
443
|
if (btn.active) a.setAttribute("aria-current", "page");
|
|
440
444
|
a.appendChild(makeIcon(btn.id === "operate" ? "zap" : "eye"));
|
|
441
445
|
a.appendChild(document.createTextNode(btn.label));
|
|
@@ -61,6 +61,12 @@ export const uiState = {
|
|
|
61
61
|
dagNodeInputData: null,
|
|
62
62
|
dagNodeInputError: null,
|
|
63
63
|
dagNodeInputLoading: false,
|
|
64
|
+
/** Node-process request identity ("<dagRunId>:<nodeId>:<key>#<seq>"). */
|
|
65
|
+
dagNodeProcessIdentity: null,
|
|
66
|
+
dagNodeProcessRequestSeq: 0,
|
|
67
|
+
dagNodeProcessData: null,
|
|
68
|
+
dagNodeProcessError: null,
|
|
69
|
+
dagNodeProcessLoading: false,
|
|
64
70
|
/**
|
|
65
71
|
* Execution-output request identity ("<dagRunId>:<nodeId>:<key>#<seq>").
|
|
66
72
|
* Stale async responses are rejected when this no longer matches.
|
|
@@ -222,6 +228,11 @@ export function makeDagNodeInputIdentity(dagRunId, nodeId) {
|
|
|
222
228
|
return `${dagRunId}:${nodeId}#${uiState.dagNodeInputRequestSeq}`;
|
|
223
229
|
}
|
|
224
230
|
|
|
231
|
+
export function makeDagNodeProcessIdentity(dagRunId, nodeId, key) {
|
|
232
|
+
uiState.dagNodeProcessRequestSeq += 1;
|
|
233
|
+
return `${dagRunId}:${nodeId}:${key ?? "latest"}#${uiState.dagNodeProcessRequestSeq}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
225
236
|
/** Build a fresh execution-output identity token (includes key). */
|
|
226
237
|
export function makeDagNodeExecutionOutputIdentity(dagRunId, nodeId, key) {
|
|
227
238
|
uiState.dagNodeExecutionOutputRequestSeq += 1;
|
|
@@ -2111,6 +2111,9 @@ body.is-resizing-dag-graph {
|
|
|
2111
2111
|
background: transparent;
|
|
2112
2112
|
color: inherit;
|
|
2113
2113
|
}
|
|
2114
|
+
.markdown-output.json-output pre {
|
|
2115
|
+
white-space: pre;
|
|
2116
|
+
}
|
|
2114
2117
|
.markdown-output a {
|
|
2115
2118
|
color: var(--orange-active);
|
|
2116
2119
|
text-decoration-color: var(--orange);
|
|
@@ -11,14 +11,15 @@ import {
|
|
|
11
11
|
} from "../constants.js";
|
|
12
12
|
import { clearNode, el } from "../dom.js";
|
|
13
13
|
import { attemptFailureLabel } from "../dag-history-labels.js";
|
|
14
|
-
import { fetchJson, fetchSpecEvidenceFile, fetchDagNodeInput } from "../api.js";
|
|
15
|
-
import { badge, formatMs } from "../format.js";
|
|
16
|
-
import { renderMarkdown } from "../markdown-render.js";
|
|
14
|
+
import { fetchJson, fetchSpecEvidenceFile, fetchDagNodeInput, fetchDagNodeProcess } from "../api.js";
|
|
15
|
+
import { badge, formatMs, prettyJsonBlock } from "../format.js";
|
|
16
|
+
import { renderMarkdown, renderInspectorDocument } from "../markdown-render.js";
|
|
17
17
|
import {
|
|
18
18
|
uiState,
|
|
19
19
|
makeSessionEventIdentity,
|
|
20
20
|
makeDagNodeInputIdentity,
|
|
21
21
|
makeDagNodeExecutionOutputIdentity,
|
|
22
|
+
makeDagNodeProcessIdentity,
|
|
22
23
|
getDagNodeExecutionOutputCache,
|
|
23
24
|
setDagNodeExecutionOutputCache,
|
|
24
25
|
clearDagNodeExecutionOutputCache,
|
|
@@ -68,6 +69,10 @@ function resetInspectorNodeSelection(dagRunId, nodeId) {
|
|
|
68
69
|
uiState.dagNodeInputData = null;
|
|
69
70
|
uiState.dagNodeInputError = null;
|
|
70
71
|
uiState.dagNodeInputLoading = false;
|
|
72
|
+
uiState.dagNodeProcessData = null;
|
|
73
|
+
uiState.dagNodeProcessError = null;
|
|
74
|
+
uiState.dagNodeProcessLoading = false;
|
|
75
|
+
uiState.dagNodeProcessIdentity = null;
|
|
71
76
|
// Drop execution-output cache/identity so node A's bodies never leak into B.
|
|
72
77
|
clearDagNodeExecutionOutputCache();
|
|
73
78
|
// Bump the session-event identity token so any in-flight response for the
|
|
@@ -748,6 +753,12 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
748
753
|
(shell.gates ?? []).join(", ") || "(none)",
|
|
749
754
|
null,
|
|
750
755
|
);
|
|
756
|
+
appendMetaRow(
|
|
757
|
+
shellDl,
|
|
758
|
+
"handlers",
|
|
759
|
+
(shell.handlers ?? []).join(", ") || "(none)",
|
|
760
|
+
null,
|
|
761
|
+
);
|
|
751
762
|
section.appendChild(shellDl);
|
|
752
763
|
if ((shell.commands ?? []).length > 0) {
|
|
753
764
|
const list = el("ol", "node-input-command-list");
|
|
@@ -1305,7 +1316,7 @@ function appendOutputBody(content, outputText, errorText, options = null) {
|
|
|
1305
1316
|
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1306
1317
|
return;
|
|
1307
1318
|
}
|
|
1308
|
-
if (outputText) content.appendChild(
|
|
1319
|
+
if (outputText) content.appendChild(renderInspectorDocument(outputText));
|
|
1309
1320
|
// Truncation note only for full/cached execution-output bodies (AC-003);
|
|
1310
1321
|
// preview path never invents truncated/maxBytes.
|
|
1311
1322
|
if (options?.truncated === true) {
|
|
@@ -1321,7 +1332,7 @@ function appendOutputBody(content, outputText, errorText, options = null) {
|
|
|
1321
1332
|
if (errorText) {
|
|
1322
1333
|
const error = el("section", "markdown-error");
|
|
1323
1334
|
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
1324
|
-
error.appendChild(
|
|
1335
|
+
error.appendChild(renderInspectorDocument(errorText));
|
|
1325
1336
|
content.appendChild(error);
|
|
1326
1337
|
}
|
|
1327
1338
|
}
|
|
@@ -1455,6 +1466,228 @@ function repaintOutputTabBody(content, dagRunId, node) {
|
|
|
1455
1466
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "output");
|
|
1456
1467
|
}
|
|
1457
1468
|
|
|
1469
|
+
function processEmptyMessage(body) {
|
|
1470
|
+
const reason = body?.emptyReason;
|
|
1471
|
+
if (reason === "static-no-process") return UI_TEXT.staticNoProcess;
|
|
1472
|
+
if (reason === "shell-receipt-missing") return UI_TEXT.noShellProcess;
|
|
1473
|
+
if (reason === "shell-preset-pending") return UI_TEXT.noPresetProcess;
|
|
1474
|
+
if (reason === "pi-session") return UI_TEXT.noSessionEvents;
|
|
1475
|
+
return UI_TEXT.noSessionEvents;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function formatProcessBlock(text) {
|
|
1479
|
+
if (!text) return "";
|
|
1480
|
+
return prettyJsonBlock(text) ?? text;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function appendProcessPre(parent, text) {
|
|
1484
|
+
if (!text) return;
|
|
1485
|
+
const pre = el("pre", "process-timeline-code");
|
|
1486
|
+
pre.textContent = formatProcessBlock(text);
|
|
1487
|
+
parent.appendChild(pre);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function renderProcessCommandCard(command) {
|
|
1491
|
+
const failed = command.ok === false;
|
|
1492
|
+
const card = el(
|
|
1493
|
+
"div",
|
|
1494
|
+
`process-timeline-card process-timeline-tool-card${failed ? " is-error" : ""}`,
|
|
1495
|
+
);
|
|
1496
|
+
const details = document.createElement("details");
|
|
1497
|
+
details.className = "process-timeline-details";
|
|
1498
|
+
details.open = failed;
|
|
1499
|
+
const summary = el("summary", "process-timeline-summary");
|
|
1500
|
+
summary.appendChild(
|
|
1501
|
+
el("span", "process-timeline-summary-title", command.command || "command"),
|
|
1502
|
+
);
|
|
1503
|
+
summary.appendChild(
|
|
1504
|
+
el(
|
|
1505
|
+
"span",
|
|
1506
|
+
`process-timeline-summary-status status-${failed ? "failed" : "succeeded"}`,
|
|
1507
|
+
failed ? "失败" : command.reused ? "复用" : "成功",
|
|
1508
|
+
),
|
|
1509
|
+
);
|
|
1510
|
+
details.appendChild(summary);
|
|
1511
|
+
const body = el("div", "process-timeline-card-body");
|
|
1512
|
+
const meta = [
|
|
1513
|
+
command.exitCode != null ? `exit ${command.exitCode}` : null,
|
|
1514
|
+
command.durationMs != null ? `耗时 ${formatMs(command.durationMs)}` : null,
|
|
1515
|
+
command.failureCategory ? command.failureCategory : null,
|
|
1516
|
+
].filter(Boolean);
|
|
1517
|
+
if (meta.length > 0) {
|
|
1518
|
+
body.appendChild(el("div", "process-timeline-section-label", meta.join(" · ")));
|
|
1519
|
+
}
|
|
1520
|
+
if (command.stdout) {
|
|
1521
|
+
body.appendChild(el("div", "process-timeline-section-label", "stdout"));
|
|
1522
|
+
appendProcessPre(body, command.stdout);
|
|
1523
|
+
}
|
|
1524
|
+
if (command.stderr) {
|
|
1525
|
+
body.appendChild(el("div", "process-timeline-section-label", "stderr"));
|
|
1526
|
+
appendProcessPre(body, command.stderr);
|
|
1527
|
+
}
|
|
1528
|
+
if (command.truncated) {
|
|
1529
|
+
body.appendChild(el("p", "muted", "内容过长,已截断。"));
|
|
1530
|
+
}
|
|
1531
|
+
details.appendChild(body);
|
|
1532
|
+
card.appendChild(details);
|
|
1533
|
+
return card;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function renderProcessStepCard(step) {
|
|
1537
|
+
const failed = step.ok === false;
|
|
1538
|
+
const card = el(
|
|
1539
|
+
"div",
|
|
1540
|
+
`process-timeline-card process-timeline-tool-card${failed ? " is-error" : ""}`,
|
|
1541
|
+
);
|
|
1542
|
+
const details = document.createElement("details");
|
|
1543
|
+
details.className = "process-timeline-details";
|
|
1544
|
+
details.open = true;
|
|
1545
|
+
const summary = el("summary", "process-timeline-summary");
|
|
1546
|
+
summary.appendChild(
|
|
1547
|
+
el("span", "process-timeline-summary-title", step.label || step.id || "step"),
|
|
1548
|
+
);
|
|
1549
|
+
summary.appendChild(
|
|
1550
|
+
el(
|
|
1551
|
+
"span",
|
|
1552
|
+
`process-timeline-summary-status status-${failed ? "failed" : "succeeded"}`,
|
|
1553
|
+
failed ? "失败" : "成功",
|
|
1554
|
+
),
|
|
1555
|
+
);
|
|
1556
|
+
details.appendChild(summary);
|
|
1557
|
+
const body = el("div", "process-timeline-card-body");
|
|
1558
|
+
if (step.durationMs != null) {
|
|
1559
|
+
body.appendChild(
|
|
1560
|
+
el("div", "process-timeline-section-label", `耗时 ${formatMs(step.durationMs)}`),
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
if (step.detail) appendProcessPre(body, step.detail);
|
|
1564
|
+
if (step.truncated) {
|
|
1565
|
+
body.appendChild(el("p", "muted", "内容过长,已截断。"));
|
|
1566
|
+
}
|
|
1567
|
+
details.appendChild(body);
|
|
1568
|
+
card.appendChild(details);
|
|
1569
|
+
return card;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function renderNonPiProcess(content, body) {
|
|
1573
|
+
content.classList.add("process-timeline-scroll");
|
|
1574
|
+
if (!body || body.emptyReason || ((body.commands ?? []).length === 0 && (body.steps ?? []).length === 0)) {
|
|
1575
|
+
content.appendChild(el("p", "empty", processEmptyMessage(body)));
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const heading =
|
|
1579
|
+
body.kind === "shell-preset"
|
|
1580
|
+
? UI_TEXT.shellPresetHeading
|
|
1581
|
+
: UI_TEXT.shellCommandsHeading;
|
|
1582
|
+
if (body.handler) {
|
|
1583
|
+
content.appendChild(
|
|
1584
|
+
el("p", "process-timeline-note", `${heading} · ${body.handler}`),
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
const ul = el("ul", "process-timeline process-timeline-rich");
|
|
1588
|
+
const items = (body.commands ?? []).length > 0 ? body.commands : body.steps ?? [];
|
|
1589
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
1590
|
+
const item = items[index];
|
|
1591
|
+
const failed = item.ok === false;
|
|
1592
|
+
const li = el(
|
|
1593
|
+
"li",
|
|
1594
|
+
`process-timeline-item process-timeline-tool-end${failed ? " is-error" : ""}${index === 0 ? " is-first" : ""}${index === items.length - 1 ? " is-last" : ""}`,
|
|
1595
|
+
);
|
|
1596
|
+
const rail = el("span", "process-timeline-rail");
|
|
1597
|
+
rail.appendChild(el("span", "process-timeline-dot"));
|
|
1598
|
+
li.appendChild(rail);
|
|
1599
|
+
const cardBody = el("div", "process-timeline-body");
|
|
1600
|
+
cardBody.appendChild(
|
|
1601
|
+
body.kind === "shell-preset"
|
|
1602
|
+
? renderProcessStepCard(item)
|
|
1603
|
+
: renderProcessCommandCard(item),
|
|
1604
|
+
);
|
|
1605
|
+
li.appendChild(cardBody);
|
|
1606
|
+
ul.appendChild(li);
|
|
1607
|
+
}
|
|
1608
|
+
content.appendChild(ul);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
export async function mergeNodeProcess(dagRunId, node) {
|
|
1612
|
+
if (!dagRunId || !node?.nodeId) return;
|
|
1613
|
+
const nodeId = node.nodeId;
|
|
1614
|
+
const key = effectiveExecutionKey(dagRunId, node);
|
|
1615
|
+
const identity = makeDagNodeProcessIdentity(dagRunId, nodeId, key);
|
|
1616
|
+
uiState.dagNodeProcessIdentity = identity;
|
|
1617
|
+
uiState.dagNodeProcessLoading = true;
|
|
1618
|
+
const result = await fetchDagNodeProcess(dagRunId, nodeId, key);
|
|
1619
|
+
if (uiState.dagNodeProcessIdentity !== identity) return;
|
|
1620
|
+
if (!result.ok || !result.body) {
|
|
1621
|
+
uiState.dagNodeProcessError =
|
|
1622
|
+
result.body?.error || UI_TEXT.processError;
|
|
1623
|
+
uiState.dagNodeProcessData = {
|
|
1624
|
+
schemaVersion: 1,
|
|
1625
|
+
dagRunId,
|
|
1626
|
+
nodeId,
|
|
1627
|
+
kind: "none",
|
|
1628
|
+
executor: node.executor ?? null,
|
|
1629
|
+
handler: null,
|
|
1630
|
+
emptyReason: "not-executed",
|
|
1631
|
+
key: key ?? null,
|
|
1632
|
+
commands: [],
|
|
1633
|
+
steps: [],
|
|
1634
|
+
};
|
|
1635
|
+
uiState.dagNodeProcessLoading = false;
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
uiState.dagNodeProcessError = null;
|
|
1639
|
+
uiState.dagNodeProcessData = result.body;
|
|
1640
|
+
uiState.dagNodeProcessLoading = false;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
1644
|
+
content.dataset.dagRunId = String(dagRunId ?? "");
|
|
1645
|
+
content.dataset.nodeId = String(node.nodeId ?? "");
|
|
1646
|
+
const key = effectiveExecutionKey(dagRunId, node);
|
|
1647
|
+
const cached = uiState.dagNodeProcessData;
|
|
1648
|
+
const sameNode =
|
|
1649
|
+
cached &&
|
|
1650
|
+
cached.dagRunId === dagRunId &&
|
|
1651
|
+
cached.nodeId === node.nodeId &&
|
|
1652
|
+
(cached.key ?? null) === (key ?? null);
|
|
1653
|
+
if (sameNode && uiState.dagNodeProcessError) {
|
|
1654
|
+
content.appendChild(el("p", "empty", uiState.dagNodeProcessError));
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (sameNode && cached.kind === "pi-session") {
|
|
1658
|
+
renderSessionTimeline(content, node.nodeId, {
|
|
1659
|
+
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
1660
|
+
});
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (sameNode && cached.kind !== "pi-session") {
|
|
1664
|
+
renderNonPiProcess(content, cached);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if ((node.executor ?? "pi") === "pi") {
|
|
1668
|
+
renderSessionTimeline(content, node.nodeId, {
|
|
1669
|
+
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
1670
|
+
});
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (uiState.dagNodeProcessError && sameNode) {
|
|
1674
|
+
content.appendChild(el("p", "empty", uiState.dagNodeProcessError));
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const loading = el("p", "muted", UI_TEXT.processLoading);
|
|
1678
|
+
loading.setAttribute("role", "status");
|
|
1679
|
+
content.appendChild(loading);
|
|
1680
|
+
void mergeNodeProcess(dagRunId, node).then(() => {
|
|
1681
|
+
if (
|
|
1682
|
+
uiState.dagInspectorTab === "timeline" &&
|
|
1683
|
+
uiState.selectedDagNodeId === node.nodeId &&
|
|
1684
|
+
uiState.currentDagRunId === dagRunId
|
|
1685
|
+
) {
|
|
1686
|
+
renderDagInspectorForCurrent();
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1458
1691
|
function fillDagInspectorContent(content, dagRunId, node) {
|
|
1459
1692
|
const previousRenderedNode = content.dataset.dagNodeId ?? "";
|
|
1460
1693
|
const previousRenderedRun = content.dataset.dagRunId ?? "";
|
|
@@ -1504,9 +1737,7 @@ function fillDagInspectorContent(content, dagRunId, node) {
|
|
|
1504
1737
|
} else if (activeTab === "timeline") {
|
|
1505
1738
|
const attemptBuilt = buildAttemptBar(dagRunId, node);
|
|
1506
1739
|
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
1507
|
-
|
|
1508
|
-
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
1509
|
-
});
|
|
1740
|
+
renderExecutorAwareTimeline(content, dagRunId, node);
|
|
1510
1741
|
} else if (activeTab === "input") {
|
|
1511
1742
|
void renderDagNodeInput(content, dagRunId, node);
|
|
1512
1743
|
} else {
|
|
@@ -1659,7 +1890,14 @@ function dagInspectorSignature(dagRunId, node) {
|
|
|
1659
1890
|
uiState.dagNodeExecutionOutputLoading ? "loading" : "idle",
|
|
1660
1891
|
]
|
|
1661
1892
|
: tab === "timeline"
|
|
1662
|
-
? [
|
|
1893
|
+
? [
|
|
1894
|
+
uiState.sessionEventOffset,
|
|
1895
|
+
uiState.dagNodeProcessData?.kind ?? "",
|
|
1896
|
+
uiState.dagNodeProcessData?.emptyReason ?? "",
|
|
1897
|
+
(uiState.dagNodeProcessData?.commands ?? []).length,
|
|
1898
|
+
(uiState.dagNodeProcessData?.steps ?? []).length,
|
|
1899
|
+
uiState.dagNodeProcessLoading ? "loading" : "idle",
|
|
1900
|
+
]
|
|
1663
1901
|
: [node.status ?? ""];
|
|
1664
1902
|
// Attempt dimension: selection identity + history shape. Poll refreshes keep
|
|
1665
1903
|
// the DOM while selection/history are unchanged; switching attempt or a
|
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
closeDagInspector,
|
|
91
91
|
selectDagNode,
|
|
92
92
|
renderDagInspector,
|
|
93
|
+
mergeNodeProcess,
|
|
93
94
|
} from "./dag-inspector.js";
|
|
94
95
|
import { mergeSessionEvents } from "./session-timeline.js";
|
|
95
96
|
|
|
@@ -451,13 +452,19 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
451
452
|
nodesEl.scrollLeft = nodesScrollLeft;
|
|
452
453
|
|
|
453
454
|
if (uiState.dagInspectorOpen && uiState.selectedDagNodeId) {
|
|
455
|
+
const selectedNode = nodes.find(
|
|
456
|
+
(node) => node.nodeId === uiState.selectedDagNodeId,
|
|
457
|
+
);
|
|
454
458
|
if (!uiState.sessionEventIdentity) {
|
|
455
459
|
uiState.sessionEventIdentity = makeSessionEventIdentity(
|
|
456
460
|
dagRunId,
|
|
457
461
|
uiState.selectedDagNodeId,
|
|
458
462
|
);
|
|
459
463
|
}
|
|
460
|
-
await
|
|
464
|
+
await Promise.all([
|
|
465
|
+
mergeSessionEvents(dagRunId, uiState.selectedDagNodeId),
|
|
466
|
+
selectedNode ? mergeNodeProcess(dagRunId, selectedNode) : Promise.resolve(),
|
|
467
|
+
]);
|
|
461
468
|
}
|
|
462
469
|
renderDagInspector(
|
|
463
470
|
dagRunId,
|
|
@@ -1500,8 +1500,26 @@ print(json.dumps(out,ensure_ascii=True))
|
|
|
1500
1500
|
// Windows Python may expose stdin as GBK; decoding generated Chinese/emoji
|
|
1501
1501
|
// source through that locale can create surrogateescape code points and make
|
|
1502
1502
|
// ast.parse fail with "surrogates not allowed".
|
|
1503
|
-
|
|
1504
|
-
|
|
1503
|
+
// POSIX images usually expose only `python3` (modern macOS has no `python`);
|
|
1504
|
+
// Windows often has a real `python` plus a Microsoft Store `python3` stub.
|
|
1505
|
+
// A missing interpreter must not silently degrade to empty payload shapes.
|
|
1506
|
+
const interpreterCandidates = process.platform === "win32" ? ["python", "python3"] : ["python3", "python"];
|
|
1507
|
+
let result;
|
|
1508
|
+
for (const interpreter of interpreterCandidates) {
|
|
1509
|
+
const attempt = spawnSync(interpreter, ["-c", program], {
|
|
1510
|
+
input: Buffer.from(source, "utf8"),
|
|
1511
|
+
encoding: "utf8",
|
|
1512
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1513
|
+
windowsHide: true,
|
|
1514
|
+
});
|
|
1515
|
+
// Interpreter missing/unspawnable → try the next candidate. A spawned
|
|
1516
|
+
// interpreter that exits nonzero analyzed real input; do not retry then.
|
|
1517
|
+
if (attempt.error)
|
|
1518
|
+
continue;
|
|
1519
|
+
result = attempt;
|
|
1520
|
+
break;
|
|
1521
|
+
}
|
|
1522
|
+
if (!result || result.status !== 0 || !result.stdout.trim())
|
|
1505
1523
|
return new Map();
|
|
1506
1524
|
try {
|
|
1507
1525
|
const parsed = JSON.parse(result.stdout);
|
|
@@ -45,6 +45,13 @@ const RAW_TO_NORMALIZED = {
|
|
|
45
45
|
quota: "executor",
|
|
46
46
|
"rate-limit": "executor",
|
|
47
47
|
unavailable: "executor",
|
|
48
|
+
// Deterministic capability/policy refusal: missing verified launcher JS
|
|
49
|
+
// entry, capability allowlist rejection, or missing bounded write tool
|
|
50
|
+
// profile. These are environment/config defects the operator repairs
|
|
51
|
+
// before rerunning; classify as executor so recovery recommends
|
|
52
|
+
// rerun-after-fix WITHOUT forcing a human-decision gate on
|
|
53
|
+
// standaloneTaskRerun (run-failed same-task rerun).
|
|
54
|
+
"tool-policy": "executor",
|
|
48
55
|
unknown: "unknown",
|
|
49
56
|
};
|
|
50
57
|
function isFinishedStatus(status) {
|