diff --git a/scripts/src/gh.ts b/scripts/src/gh.ts index b75878ee..c419ce37 100644 --- a/scripts/src/gh.ts +++ b/scripts/src/gh.ts @@ -4711,6 +4711,22 @@ function compactCommentSummary(comment: GitHubComment): Record }; } +function compactIssueViewCommentSummary(comment: GitHubComment): Record { + const body = comment.body ?? ""; + return { + id: comment.id, + url: comment.html_url, + author: comment.user?.login ?? null, + createdAt: comment.created_at ?? null, + updatedAt: comment.updated_at ?? null, + bodyChars: body.length, + bodySha: bodySha(body), + bodyPreview: ghShort(preview(body).replace(/\s+/gu, " "), 200), + bodyOmitted: true, + fullBodyIncluded: false, + }; +} + function prStateDetail(pr: GitHubPullRequest): "open" | "closed" | "merged" { if (pr.merged === true || pr.merged_at !== null && pr.merged_at !== undefined) return "merged"; return pr.state === "closed" ? "closed" : "open"; @@ -6019,13 +6035,13 @@ async function getIssueComment(token: string, repo: string, commentId: number): return githubRequest(token, "GET", `/repos/${owner}/${name}/issues/comments/${commentId}`); } -function selectedIssueJson(issue: GitHubIssue, comments: GitHubComment[] | null, fields: IssueViewJsonField[] | undefined): Record | null { +function selectedIssueJson(issue: GitHubIssue, comments: GitHubComment[] | null, fields: IssueViewJsonField[] | undefined, options: { includeFullCommentBodies?: boolean } = {}): Record | null { if (fields === undefined) return null; const summary = issueSummary(issue); const selected: Record = {}; for (const field of fields) { if (field === "comments") { - selected.comments = (comments ?? []).map(commentSummary); + selected.comments = (comments ?? []).map(options.includeFullCommentBodies === true ? commentSummary : compactIssueViewCommentSummary); } else { selected[field] = summary[field]; } @@ -6033,11 +6049,12 @@ function selectedIssueJson(issue: GitHubIssue, comments: GitHubComment[] | null, return selected; } -async function issueRead(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, commandName = "issue read", disclosure: Record | null = null): Promise { +async function issueRead(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, commandName = "issue read", disclosure: Record | null = null, options: { includeFullCommentBodies?: boolean } = {}): Promise { const issue = await getIssue(token, repo, issueNumber); if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber }); const needsComments = jsonFields === undefined || jsonFields.includes("comments"); const includeBody = jsonFields === undefined || jsonFields.includes("body"); + const includeFullCommentBodies = options.includeFullCommentBodies === true; const comments = needsComments ? await listIssueComments(token, repo, issueNumber) : null; if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue, { includeBody, includePreview: false }) }); return { @@ -6047,21 +6064,28 @@ async function issueRead(repo: string, token: string, issueNumber: number, jsonF ...(disclosure === null ? {} : { disclosure }), issue: issueSummary(issue, { includeBody, includePreview: false }), codeQueueBoardHint: codeQueueBoardCommanderBriefHint(issueNumber, issue.body ?? ""), - ...(comments === null ? {} : { comments: comments.map(commentSummary) }), + ...(comments === null || jsonFields !== undefined ? {} : { comments: comments.map(includeFullCommentBodies ? commentSummary : compactCommentSummary) }), ...(jsonFields === undefined ? {} : { jsonFields, - json: selectedIssueJson(issue, comments, jsonFields), + json: selectedIssueJson(issue, comments, jsonFields, { includeFullCommentBodies }), compatibility: { legacyJsonBodyPath: includeBody ? ".data.issue.body" : null, bodyOmitted: !includeBody, - readCommands: includeBody ? null : issueBodyReadCommands(repo, issueNumber), + commentsCompacted: comments !== null && !includeFullCommentBodies, + commentBodiesOmitted: comments !== null && !includeFullCommentBodies, + fullCommentBodiesIncluded: comments !== null && includeFullCommentBodies, + commentsPath: comments === null ? null : ".data.json.comments", + readCommands: { + ...(includeBody ? {} : issueBodyReadCommands(repo, issueNumber)), + ...(comments !== null && !includeFullCommentBodies ? issueCommentReadCommands(repo, issueNumber) : {}), + }, }, }), }; } -async function issueView(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, disclosure: Record | null = null): Promise { - return issueRead(repo, token, issueNumber, jsonFields, "issue view", disclosure); +async function issueView(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, disclosure: Record | null = null, options: { includeFullCommentBodies?: boolean } = {}): Promise { + return issueRead(repo, token, issueNumber, jsonFields, "issue view", disclosure, options); } const GITHUB_USER_ATTACHMENT_URL_PATTERN = /https:\/\/github\.com\/user-attachments\/assets\/[A-Za-z0-9._-]+(?:\?[^\s<>"')]+)?/giu; @@ -8653,8 +8677,8 @@ export async function runGhCommand(args: string[]): Promise, scoped: ReturnType): RenderedCliResult { + const summary = record(result.summary); + const pipelineRun = record(summary.pipelineRun); + const argo = record(summary.argo); + const runtimeSummary = record(summary.runtime); + const publicProbe = record(summary.publicProbe); + const gitMirror = record(summary.gitMirror); + const runtime = record(result.runtime); + const bridge = record(runtime.externalPostgresBridge); + const secrets = record(runtime.externalPostgresSecrets); + const next = record(summary.next); + const workloadReadiness = Array.isArray(runtime.workloadReadiness) ? runtime.workloadReadiness.map(record) : []; + const workloadRows = workloadReadiness.length === 0 + ? [["-", "-", "-", "-", "-"]] + : workloadReadiness.slice(0, 18).map((item) => [ + webObserveText(item.ref), + item.ready === true ? "ok" : "not-ready", + webObserveText(item.readyReplicas), + webObserveText(item.currentReplicas), + webObserveText(item.desiredReplicas), + ]); + const bridgeRows = bridge.required === false + ? [["not-required", "-", "-", "-", "-"]] + : [[ + bridge.ready === true ? "ok" : "failed", + webObserveText(bridge.serviceName), + webObserveText(bridge.endpointAddress), + webObserveText(bridge.port), + `${webObserveText(bridge.expectedEndpointAddress)}:${webObserveText(bridge.expectedPort)}`, + ]]; + const secretRows = nodeRuntimeExternalPostgresSecretRows(secrets); + const status = result.ok === true ? "ok" : result.ok === false ? "failed" : "-"; + const renderedText = [ + "hwlab nodes control-plane status --full", + "", + webObserveTable( + ["NODE", "LANE", "STATUS", "REASON", "SOURCE", "PIPELINERUN"], + [[ + scoped.node, + scoped.lane, + status, + webObserveShort(webObserveText(summary.degradedReason ?? result.degradedReason), 40), + shortValue(summary.sourceCommit ?? result.sourceCommit), + webObserveShort(webObserveText(pipelineRun.name ?? result.pipelineRun), 36), + ]], + ), + "", + webObserveTable( + ["STAGE", "STATUS", "DETAIL"], + [ + ["pipeline", pipelineRun.ready === true ? "ok" : pipelineRun.exists === true ? webObserveText(pipelineRun.status) : "missing", webObserveShort(webObserveText(pipelineRun.reason ?? pipelineRun.message), 80)], + ["argo", argo.ready === true ? "ok" : "failed", `${webObserveText(argo.syncStatus)}/${webObserveText(argo.health)} rev=${shortValue(argo.syncRevision)}`], + ["runtime", runtimeSummary.ready === true ? "ok" : "failed", `namespace=${webObserveText(runtimeSummary.namespace)} workloads=${webObserveText(runtimeSummary.workloadReady)} pg=${webObserveText(runtimeSummary.externalPostgresReady ?? runtimeSummary.localPostgresReady)}`], + ["public", publicProbe.ready === true ? "ok" : "failed", webObserveShort(webObserveText(record(publicProbe.diagnostic).kind ?? record(publicProbe.diagnostic).message), 80)], + ["git-mirror", gitMirror.ready === true ? "ok" : "failed", `pending=${webObserveText(gitMirror.pendingFlush)} inSync=${webObserveText(gitMirror.githubInSync)}`], + ], + ), + "", + "RUNTIME_WORKLOADS", + webObserveTable(["REF", "READY", "READY_REPLICAS", "CURRENT", "DESIRED"], workloadRows), + ...(workloadReadiness.length > workloadRows.length ? [` ... ${workloadReadiness.length - workloadRows.length} more workloads omitted; use --raw for complete JSON.`] : []), + "", + "EXTERNAL_POSTGRES_BRIDGE", + webObserveTable(["STATUS", "SERVICE", "ENDPOINT", "PORT", "EXPECTED"], bridgeRows), + "", + "EXTERNAL_POSTGRES_SECRETS", + webObserveTable(["ROLE", "SECRET", "KEY", "PRESENT", "VALUES_PRINTED"], secretRows), + "", + "NEXT", + ` ${summary.nextAction ?? next.full ?? `bun scripts/cli.ts hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane} --full`}`, + ` bun scripts/cli.ts hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane} --raw`, + "", + "Disclosure:", + " --full status is a bounded runtime-workloads drill-down view; use --raw only when complete JSON is required.", + " Secret evidence is limited to object metadata, key names, presence, and valuesPrinted=false.", + ].join("\n"); + return { ...result, renderedText, contentType: "text/plain" }; +} + +function nodeRuntimeExternalPostgresSecretRows(secrets: Record): unknown[][] { + if (secrets.required === false) return [["not-required", "-", "-", "-", "false"]]; + if (secrets.required === true && secrets.ready !== true && secrets.degradedReason !== undefined) { + return [[webObserveText(secrets.degradedReason), "-", "-", "false", "false"]]; + } + const cloudApi = record(secrets.cloudApi); + const openfga = record(secrets.openfga); + const datastoreUri = record(openfga.datastoreUri); + const authnPresharedKey = record(openfga.authnPresharedKey); + return [ + ["cloudApi", webObserveText(cloudApi.secret), webObserveText(cloudApi.key), webObserveText(cloudApi.present), webObserveText(cloudApi.valuesPrinted ?? false)], + ["openfga.datastoreUri", webObserveText(datastoreUri.secret), webObserveText(datastoreUri.key), webObserveText(datastoreUri.present), webObserveText(datastoreUri.valuesPrinted ?? false)], + ["openfga.authnPresharedKey", webObserveText(authnPresharedKey.secret), webObserveText(authnPresharedKey.key), webObserveText(authnPresharedKey.present), webObserveText(authnPresharedKey.valuesPrinted ?? false)], + ]; +} + function nodeRuntimeGitMirrorStatus(scoped: ReturnType): Record { const spec = scoped.spec; const mirror = nodeRuntimeGitMirrorTarget(spec); diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts index 124cc4ef..e3d01257 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -1402,7 +1402,7 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN if (transientCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-transient-divergence", severity: "info", summary: "control and observer pages briefly differed near a sampled transition; retained as transient evidence but not treated as persistent projection failure", count: transientCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: transientCrossPageProjectionDiffs.slice(0, 20) }); if (crossPageTraceVisibilityDiffs.length > 0) findings.push({ id: "cross-page-trace-visibility-divergence", severity: "info", summary: "control and observer pages differed only in visible trace row count; this is local disclosure/hydration visibility, not session/message projection divergence", count: crossPageTraceVisibilityDiffs.length, samples: crossPageTraceVisibilityDiffs.slice(0, 20) }); const traceMessageDuplicates = detectTraceMessageDuplication(samples); - if (traceMessageDuplicates.length > 0) findings.push({ id: "trace-assistant-message-duplicates-final-response", severity: "amber", summary: "trace rendered assistant message rows that duplicate the sealed final response", count: traceMessageDuplicates.length, samples: traceMessageDuplicates.slice(0, 20) }); + if (traceMessageDuplicates.length > 0) findings.push({ id: "trace-assistant-message-duplicates-final-response", severity: "amber", summary: "trace-frame rendered duplicate visible assistant final rows; the fixed Final Response renderer summary block is excluded", count: traceMessageDuplicates.length, finalResponseSummaryBlockCounted: false, traceFrameSource: "traceRows-only", samples: traceMessageDuplicates.slice(0, 20) }); const turnTraceMissing = detectTurnTraceIdMissing(samples); if (turnTraceMissing.length > 0) findings.push({ id: "turn-trace-id-missing", severity: "red", summary: "Code Agent turn/card was visible without a trace id, so historical trace hydration cannot be reliable", count: turnTraceMissing.length, samples: turnTraceMissing.slice(0, 20) }); const pagePerformanceItems = Array.isArray(pagePerformance?.sameOriginApiByPath) ? pagePerformance.sameOriginApiByPath : []; diff --git a/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts index 1f3b0f79..11881538 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts @@ -1897,17 +1897,32 @@ function detectAdjacentCrossPageProjectionDiffs(samples) { function detectTraceMessageDuplication(samples) { const rows = []; for (const sample of samples) { - const finalText = normalizedText((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textPreview || item?.text || "").join("\n")); - if (finalText.length < 40) continue; const traceRows = Array.isArray(sample?.traceRows) ? sample.traceRows : []; - for (const row of traceRows) { + const groups = new Map(); + for (let fallbackIndex = 0; fallbackIndex < traceRows.length; fallbackIndex += 1) { + const row = traceRows[fallbackIndex]; const rowTextRaw = String(row?.textPreview || row?.text || ""); if (!/(?:助手消息|assistant\s+message|assistant)/iu.test(rowTextRaw)) continue; const rowText = normalizedText(rowTextRaw); if (rowText.length < 24) continue; - const overlap = longestSharedSubstringLength(finalText, rowText); - if (overlap < 40 && overlap < Math.min(rowText.length, finalText.length) * 0.55) continue; - rows.push({ ...ref(sample), traceId: row?.traceId ?? null, rowIndex: row?.index ?? null, rowTextPreview: limitText(rowTextRaw, 180) }); + const traceId = row?.traceId === undefined || row?.traceId === null ? "" : String(row.traceId); + const key = traceId + "\u0000" + rowText; + const group = groups.get(key) ?? { traceId: traceId || null, rowText, rowTextRaw, rows: [] }; + group.rows.push({ row, fallbackIndex }); + groups.set(key, group); + } + for (const group of groups.values()) { + if (group.rows.length < 2) continue; + rows.push({ + ...ref(sample), + traceId: group.traceId, + visibleAssistantFinalRowCount: group.rows.length, + rowIndexes: group.rows.map((item) => item.row?.index ?? item.fallbackIndex).slice(0, 12), + rowTextHash: sha256(group.rowText), + rowTextPreview: limitText(group.rowTextRaw, 180), + finalResponseSummaryBlockCounted: false, + traceFrameSource: "traceRows-only" + }); } } return rows;