Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions actions/setup/js/fixtures/deepseek_ci_logs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @ts-check

// Sanitized stdout shapes from existing Actions artifacts, not new executions.
// https://github.com/github/gh-aw/actions/runs/37553714211 (agent artifact 11453917896)
const startupFailure = `[entrypoint] Executing command: dsh --profile headless "private fixture prompt"
[deepseek-harness] awf-reflect: models fetch returned 401 for http://api-proxy:10000/v1/models
[deepseek-harness] configured provider=github model=auto
file:///fixture/node_modules/@deepseek-ai/dsh-app-boot/lib/index.js:764
\tif (hmr === void 0) throw new Error(\`\${binName}: user patch-layer watching requires the Cordis HMR service\`);
\t ^

Error: dsh: user patch-layer watching requires the Cordis HMR service
at watchUserPatches (file:///fixture/node_modules/@deepseek-ai/dsh-app-boot/lib/index.js:764:28)
at runProfile (file:///fixture/node_modules/@deepseek-ai/dsh/lib/profile-boot.js:264:9)
at async file:///fixture/node_modules/@deepseek-ai/dsh/lib/bin.js:133:3

Node.js v24.21.0
[deepseek-harness] DeepSeek Harness execution failed with exit code 1
[entrypoint] Relaxed /fixture/gh-aw group permissions for host-side post-processing
[INFO] Stopping containers...
Container awf-agent Stopping
Container awf-agent Stopped
[SUCCESS] Containers stopped successfully
[WARN] Command completed with exit code: 1
Process exiting with code: 1
`;

// https://github.com/github/gh-aw/actions/runs/37866549668 (agent artifact 11589545575)
// The job timed out after 15 minutes. Its stdout ended at configuration; no
// native terminal observation, message, usage, or exit code was present there.
const timedOut = `[entrypoint] Executing command: dsh --profile headless "private fixture prompt"
[deepseek-harness] awf-reflect: models fetch returned 401 for http://api-proxy:10000/v1/models
[deepseek-harness] configured provider=github model=auto
`;

module.exports = { startupFailure, timedOut };
124 changes: 98 additions & 26 deletions actions/setup/js/parse_deepseek_log.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,136 @@

const { createEngineLogParser, generateCopilotCliStyleSummary, buildStepSummaryDetailsSection } = require("./log_parser_shared.cjs");
const { createSessionEvent } = require("./agent_session.cjs");
const { collectAgentExecution } = require("./agent_execution.cjs");
const { applyAddMaskRedaction, collectAddMaskedValues } = require("./add_mask_redaction.cjs");
const { stripVTControlCharacters } = require("node:util");

const main = createEngineLogParser({ parserName: "DeepSeek Harness", parseFunction: parseDeepSeekLog, supportsDirectories: false });
const HARNESS_FAILURE = /^\[deepseek-harness\] DeepSeek Harness execution failed with exit code ([1-9]\d*)(?: \(signal=[A-Z0-9]+\))?$/;
const TOKEN_CLEANUP = /^\[entrypoint\] Unset [A-Z][A-Z0-9_]* from \/proc\/1\/environ$/;
const PERMISSION_CLEANUP = /^\[entrypoint\] Relaxed \S+ group permissions for host-side post-processing$/;

/**
* Recognize the completed headless stdout envelope observed in real AWF runs.
* Everything before configuration and after shutdown is infrastructure, not
* conversation. Ambiguous, truncated, and prompt-labeled output is withheld.
* @param {string} content
* @returns {{model: string, content: string} | undefined}
* @param {string[]} lines
* @returns {{index: number, provider: string, model: string} | undefined}
*/
function extractHeadlessAnswer(content) {
const lines = stripVTControlCharacters(content).split(/\r?\n/);
function findConfiguration(lines) {
const configurations = [];
for (const [index, line] of lines.entries()) {
const match = line.match(/^\[deepseek-harness\] configured provider=([a-z][a-z0-9-]*) model=(\S+)$/);
if (match) configurations.push({ index, model: match[2] });
if (match) configurations.push({ index, provider: match[1], model: match[2] });
}
if (configurations.length !== 1) return;
const configuration = configurations[0];
return configurations.length === 1 ? configurations[0] : undefined;
}

/**
* Only a completed stdout envelope attributes otherwise unlabeled text to the
* assistant. Configuration and attributed failures remain usable independently.
* @param {string[]} lines
* @param {{index: number}} configuration
* @returns {string | undefined}
*/
function extractHeadlessAnswer(lines, configuration) {
const shutdown = lines.findIndex((line, index) => index > configuration.index && line === "[INFO] Stopping containers...");
if (shutdown < 0) return;
const completed = lines.findIndex((line, index) => index > shutdown && line === "[SUCCESS] Command completed successfully");
if (completed < 0 || !lines.slice(completed + 1).includes("Process exiting with code: 0")) return;
const exit = lines.findIndex((line, index) => index > shutdown && /^Process exiting with code:/.test(line));
if (completed < 0 || exit <= completed || lines[exit] !== "Process exiting with code: 0") return;

let start = configuration.index + 1;
while (
start < shutdown &&
(lines[start].trim() === "" || lines[start] === "[entrypoint] Unsetting sensitive tokens from parent shell environment..." || /^\[entrypoint\] Unset [A-Z][A-Z0-9_]* from \/proc\/1\/environ$/.test(lines[start]))
) {
while (start < shutdown && (lines[start].trim() === "" || lines[start] === "[entrypoint] Unsetting sensitive tokens from parent shell environment..." || TOKEN_CLEANUP.test(lines[start]))) {
start++;
}
const answerLines = lines.slice(start, shutdown);
while (answerLines.length && answerLines[answerLines.length - 1].trim() === "") answerLines.pop();
while (answerLines.length && (answerLines[answerLines.length - 1].trim() === "" || PERMISSION_CLEANUP.test(answerLines[answerLines.length - 1]))) {
answerLines.pop();
}
const answer = answerLines.join("\n");
if (!answer || /^[ \t]*\[(?:entrypoint|INFO|WARN|SUCCESS|deepseek-harness)\]/m.test(answer) || /^[ \t]*(?:(?:user|system)\s*(?:prompt|message)?\s*:|prompt\s*:)/im.test(answer)) return;
return { model: configuration.model, content: answer };
if (
!answer ||
answerLines.some(line => /^[ \t]*\[(?:entrypoint|health-check|INFO|WARN|ERROR|SUCCESS|deepseek-harness)\]/.test(line) && !HARNESS_FAILURE.test(line)) ||
/^ (?:Container|Network|Volume) |^Process exiting with code:|^file:\/\/\/|^Node\.js v\d/m.test(answer) ||
/^[ \t]*(?:(?:user|system)\s*(?:prompt|message)?\s*:|prompt\s*:)/im.test(answer)
) {
return;
}
return answer;
}

/**
* Node startup stacks require both the runtime framing and an attributed harness
* failure. An error quoted in the assistant answer is not engine evidence.
* @param {string[]} lines
* @param {{index: number}} configuration
* @returns {import("./types/agent_session").SessionEvent[]}
*/
function extractHeadlessErrors(lines, configuration) {
const shutdown = lines.findIndex((line, index) => index > configuration.index && line === "[INFO] Stopping containers...");
const body = lines.slice(configuration.index + 1, shutdown < 0 ? undefined : shutdown);
if (body.some(line => /^[ \t]*(?:(?:user|system)\s*(?:prompt|message)?\s*:|prompt\s*:)/i.test(line))) return [];
const failureIndex = body.findIndex(line => HARNESS_FAILURE.test(line));
if (failureIndex < 0) return [];
const exitCode = Number(body[failureIndex].match(HARNESS_FAILURE)?.[1]);
if (shutdown >= 0) {
const terminal = lines.slice(shutdown + 1);
const exit = terminal.find(line => /^Process exiting with code:/.test(line));
if (terminal.includes("[SUCCESS] Command completed successfully") || (exit !== undefined && exit !== `Process exiting with code: ${exitCode}`)) return [];
}
if (!body.slice(failureIndex + 1).every(line => line.trim() === "" || PERMISSION_CLEANUP.test(line))) return [];
const start = body.findIndex(line => line.trim() !== "" && line !== "[entrypoint] Unsetting sensitive tokens from parent shell environment..." && !TOKEN_CLEANUP.test(line));
const stackStart = body.findIndex(line => /^file:\/\/\/\S+:\d+(?::\d+)?$/.test(line));
const nodeVersion = body.findIndex((line, index) => index > stackStart && /^Node\.js v\d+\.\d+\.\d+$/.test(line));
const hasNodeStack = stackStart === start && stackStart >= 0 && nodeVersion > stackStart && nodeVersion < failureIndex && body.slice(nodeVersion + 1, failureIndex).every(line => line.trim() === "");
if (failureIndex !== start && !hasNodeStack) return [];
const entries = [];
if (hasNodeStack) {
for (let index = stackStart + 1; index < nodeVersion; index++) {
const error = body[index].match(/^([A-Za-z]*Error): (.*)$/);
if (!error || !/^ at /.test(body[index + 1] ?? "")) continue;
const stack = body.slice(index, nodeVersion);
while (stack.at(-1) === "") stack.pop();
entries.push(createSessionEvent({}, "session.error", { message: error[2], errorType: error[1], stack: stack.join("\n") }));
break;
}
}
const message = body[failureIndex].slice("[deepseek-harness] ".length);
entries.push(createSessionEvent({}, "session.error", { message, ...(exitCode <= 255 ? { exitCode } : {}) }));
const execution = collectAgentExecution({ events: entries, ...(exitCode <= 255 ? { exitCode } : {}) });
if (execution) entries.push(execution);
return entries;
}

/** @param {string} content @returns {boolean} */
function isDeepSeekLog(content) {
return extractHeadlessAnswer(content) !== undefined;
return findConfiguration(stripVTControlCharacters(content).split(/\r?\n/)) !== undefined;
}

/**
* Preserve the final headless answer as canonical assistant text. Native tool
* calls, reasoning, usage, and task success are not reported by this stdout
* format and must not be inferred from the answer or runtime MCP metadata.
* Preserve headless configuration, completed assistant text, and attributed
* startup failures. Tools, reasoning, usage, and task success are not exposed by
* this stdout profile. Canonical artifacts are handled by shared session readers,
* not interpreted as an additional native dsh stdout format.
* @param {string} content
* @returns {{markdown: string, logEntries: import("./types/agent_session").SessionEvent[], mcpFailures: string[], maxTurnsHit: boolean}}
*/
function parseDeepSeekLog(content) {
const answer = extractHeadlessAnswer(content);
const logEntries = answer ? [createSessionEvent({}, "session.init", { sourceEngine: "deepseek-harness", model: answer.model }), createSessionEvent({}, "assistant.message", { content: answer.content })] : [];
const lines = applyAddMaskRedaction(stripVTControlCharacters(content), collectAddMaskedValues(content)).split(/\r?\n/);
const configuration = findConfiguration(lines);
/** @type {import("./types/agent_session").SessionEvent[]} */
let logEntries = [];
let notice = "";
if (configuration) {
logEntries.push(createSessionEvent({}, "session.init", { sourceEngine: "deepseek-harness", provider: configuration.provider, model: configuration.model }));
Comment thread
pelikhan marked this conversation as resolved.
const errors = extractHeadlessErrors(lines, configuration);
const answer = errors.length ? undefined : extractHeadlessAnswer(lines, configuration);
if (answer !== undefined) logEntries.push(createSessionEvent({}, "assistant.message", { content: answer }));
else notice = "Headless assistant output is unavailable, incomplete, or ambiguous. Unattributed stdout is omitted; configuration and observed failures are retained.";
logEntries.push(...errors);
}
return {
markdown: logEntries.length
? generateCopilotCliStyleSummary(logEntries)
: buildStepSummaryDetailsSection("DeepSeek Harness", "Completed headless stdout envelope not recognized. Raw content is omitted because it may contain prompts or secrets."),
? generateCopilotCliStyleSummary(logEntries) + (notice ? `\n\n${buildStepSummaryDetailsSection("DeepSeek Harness", notice)}` : "")
: buildStepSummaryDetailsSection("DeepSeek Harness", "Supported headless observations not recognized. Raw content is omitted because it may contain prompts or secrets."),
logEntries,
mcpFailures: [],
maxTurnsHit: false,
Expand Down
Loading
Loading