Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Address DeepSeek parser review feedback
Keep native parsing scoped to observed headless output, removing the generic canonical fallback and its redaction bypass. Corroborate startup failure framing and wrapper outcomes so quoted harness lines remain assistant text. Support Node file headers with columns and clarify intentional provider projection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
pelikhan and Copilot committed Oct 10, 2026
commit ceca28ce876b341bcb006b7a02e82fe00add71bf
56 changes: 25 additions & 31 deletions actions/setup/js/parse_deepseek_log.cjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// @ts-check

const { createEngineLogParser, generateCopilotCliStyleSummary, buildStepSummaryDetailsSection, parseLogEntries } = require("./log_parser_shared.cjs");
const { createSessionEvent, isSessionEvent, normalizeAgentSession } = require("./agent_session.cjs");
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$/;

/**
* @param {string[]} lines
Expand Down Expand Up @@ -36,20 +39,17 @@ function extractHeadlessAnswer(lines, configuration) {
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() === "" || /^\[entrypoint\] Relaxed \S+ group permissions for host-side post-processing$/.test(answerLines[answerLines.length - 1]))) {
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|health-check|INFO|WARN|ERROR|SUCCESS|deepseek-harness)\]/m.test(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)
) {
Expand All @@ -69,12 +69,22 @@ 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 => /^\[deepseek-harness\] DeepSeek Harness execution failed with exit code [1-9]\d*(?: \(signal=[A-Z0-9]+\))?$/.test(line));
const failureIndex = body.findIndex(line => HARNESS_FAILURE.test(line));
if (failureIndex < 0) return [];
const entries = [];
const stackStart = body.findIndex(line => /^file:\/\/\/\S+:\d+$/.test(line));
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));
if (stackStart >= 0 && nodeVersion > stackStart && nodeVersion < failureIndex) {
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;
Expand All @@ -85,7 +95,6 @@ function extractHeadlessErrors(lines, configuration) {
}
}
const message = body[failureIndex].slice("[deepseek-harness] ".length);
const exitCode = Number(message.match(/exit code (\d+)/)?.[1]);
entries.push(createSessionEvent({}, "session.error", { message, ...(exitCode <= 255 ? { exitCode } : {}) }));
const execution = collectAgentExecution({ events: entries, ...(exitCode <= 255 ? { exitCode } : {}) });
if (execution) entries.push(execution);
Expand All @@ -100,8 +109,8 @@ function isDeepSeekLog(content) {
/**
* Preserve headless configuration, completed assistant text, and attributed
* startup failures. Tools, reasoning, usage, and task success are not exposed by
* this stdout profile. Direct canonical inputs remain loss-preserving; they are
* not a claimed native dsh JSON format.
* 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}}
*/
Expand All @@ -118,26 +127,11 @@ function parseDeepSeekLog(content) {
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);
} else if (!lines.some(line => /^\[deepseek-harness\]/.test(line))) {
let records = [];
let completeDocument = false;
try {
const document = JSON.parse(content);
records = Array.isArray(document) ? document : [document];
completeDocument = true;
} catch {
// A JSONL stream can retain adjacent valid records from a partial capture.
if (lines.filter(line => line.trim()).every(line => /^[ \t]*[{[]/.test(line))) records = parseLogEntries(content) ?? [];
}
logEntries = normalizeAgentSession(records.filter(isSessionEvent));
const nonblankLines = lines.filter(line => line.trim());
if (records.some(record => !isSessionEvent(record)) || (!completeDocument && records.length !== nonblankLines.length))
notice = "Some structured records were not recognized. Supported canonical observations are retained; coverage is partial.";
}
return {
markdown: logEntries.length
? generateCopilotCliStyleSummary(logEntries) + (notice ? `\n\n${buildStepSummaryDetailsSection("DeepSeek Harness", notice)}` : "")
: buildStepSummaryDetailsSection("DeepSeek Harness", "Supported headless or canonical observations not recognized. Raw content is omitted because it may contain prompts or secrets."),
: 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
93 changes: 44 additions & 49 deletions actions/setup/js/parse_deepseek_log.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import os from "node:os";
import path from "node:path";
import { parseDeepSeekLog, isDeepSeekLog } from "./parse_deepseek_log.cjs";
import { startupFailure, timedOut } from "./fixtures/deepseek_ci_logs.cjs";
import { normalizeAgentSession, projectSessionResult } from "./agent_session.cjs";
import { projectSessionResult } from "./agent_session.cjs";
import { serializeSessionArtifact, writeSessionArtifact } from "./session_artifact.cjs";
import { collectUnifiedSession, parseEngineSession, writeUnifiedSession } from "./unified_session.cjs";
import { parseCustomLog } from "./parse_custom_log.cjs";
Expand Down Expand Up @@ -156,6 +156,12 @@ describe("parseDeepSeekLog", () => {
expect(parseEngineSession(startupFailure, "deepseek-harness")).toEqual(result.logEntries);
});

it("preserves Node startup errors when the header includes a column", () => {
const raw = startupFailure.replace("lib/index.js:764\n", "lib/index.js:764:28\n");

expect(parseDeepSeekLog(raw).logEntries).toEqual(parseDeepSeekLog(startupFailure).logEntries);
});

it("retains a harness failure even without a Node stack or AWF shutdown", () => {
const result = parseDeepSeekLog(`${timedOut}[deepseek-harness] DeepSeek Harness execution failed with exit code 137 (signal=SIGKILL)\n`);

Expand All @@ -174,6 +180,34 @@ describe("parseDeepSeekLog", () => {
expect(parseDeepSeekLog(fixture).logEntries.some(event => event.type === "session.error")).toBe(false);
});

it.each(["Here is the fix.\n[deepseek-harness] DeepSeek Harness execution failed with exit code 1\nApply it.", "[deepseek-harness] DeepSeek Harness execution failed with exit code 1"])(
"preserves a quoted harness failure in completed assistant text",
text => {
const result = parseDeepSeekLog(fixture.replace(answer, text));

expect(result.logEntries).toEqual([
{ type: "session.init", data: { sourceEngine: "deepseek-harness", provider: "github", model: "claude-sonnet-4.5" } },
{ type: "assistant.message", data: { content: text } },
]);
expect(parseEngineSession(fixture.replace(answer, text), "deepseek-harness").some(event => event.type === "session.error")).toBe(false);
}
);

it.each(["Here is the fix.\n[deepseek-harness] DeepSeek Harness execution failed with exit code 1", "[deepseek-harness] DeepSeek Harness execution failed with exit code 1\nApply it."])(
"does not turn an unframed partial quote into an execution failure",
text => {
const result = parseDeepSeekLog(`${timedOut}${text}\n`);

expect(result.logEntries).toEqual([{ type: "session.init", data: { sourceEngine: "deepseek-harness", provider: "github", model: "auto" } }]);
}
);

it("does not accept quoted Node crash framing before a successful wrapper exit", () => {
const raw = startupFailure.replace("[WARN] Command completed with exit code: 1", "[SUCCESS] Command completed successfully").replace("Process exiting with code: 1", "Process exiting with code: 0");

expect(parseDeepSeekLog(raw).logEntries.map(event => event.type)).toEqual(["session.init"]);
});

it("allows the observed post-command permission cleanup without including it in assistant content", () => {
const raw = fixture.replace("[INFO] Stopping containers...", "[entrypoint] Relaxed /fixture/gh-aw group permissions for host-side post-processing\n[INFO] Stopping containers...");

Expand Down Expand Up @@ -205,54 +239,23 @@ describe("parseDeepSeekLog", () => {
expect(JSON.stringify(result)).not.toContain("fixture-secret");
expect(JSON.stringify(result)).not.toContain("::add-mask::");
});
});

// Canonical contract fixtures are synthetic; the captured dsh headless runs do
// not expose native structured messages, tool lifecycles, refusals, or usage.
describe("DeepSeek direct canonical session inputs", () => {
const events = [
{ type: "session.init", id: "native-0", parentId: null, timestamp: "2026-10-07T00:00:00Z", data: { sourceEngine: "deepseek-harness", model: "fixture-model", sessionId: "native-session", cwd: "/fixture" } },
{ type: "user.message", id: "native-1", parentId: "native-0", timestamp: 1, data: { content: "" } },
{ type: "assistant.reasoning", id: "native-2", data: { content: "hello", partial: true, messageId: "message-1", channel: "reasoning" } },
{ type: "assistant.reasoning", id: "native-3", data: { content: " \n", partial: true, messageId: "message-1", channel: "reasoning" } },
{ type: "tool.execution_start", id: "native-4", custom: { nested: false }, data: { toolCallId: "call-1", toolName: "fixture-tool", input: { empty: "", zero: 0, null: null, array: [], object: {} } } },
{ type: "tool.execution_complete", id: "native-5", parentId: "native-4", data: { toolCallId: "call-1", success: false, output: false, durationMs: 0, exitCode: 1, error: { code: "fixture-failure" } } },
{ type: "assistant.refusal", id: "native-6", data: { reason: "content_filter", content: null, policyCategory: null, explanation: "", partial: true } },
{ type: "session.error", id: "native-7", data: { error: { code: 0, message: "" } } },
{ type: "session.result", id: "native-8", data: { usage: { input_tokens: 2, output_tokens: 3 }, numTurns: 0, durationMs: 0, totalCostUsd: 0 } },
{ type: "session.result", id: "native-9", data: { usage: { total_tokens: 5 }, status: "failed", errors: [] } },
{ type: "vendor.unknown", id: "native-10", parentId: null, metadata: [false, 0, ""], data: { deep: { null: null, false: false, zero: 0, empty: [], object: {} } } },
{ type: "tool.execution_complete", data: { toolCallId: "orphan", output: 0 } },
{ type: "tool.execution_start", data: { toolCallId: "dangling", input: null } },
];

it.each(["jsonl", "array", "pretty-array"])("retains exact canonical content, metadata, values and order from %s", format => {
const raw = format === "jsonl" ? events.map(JSON.stringify).join("\n") : JSON.stringify(events, null, format === "pretty-array" ? 2 : undefined);
it("rejects unobserved canonical stdout even when interleaved with mask commands", () => {
const raw = '::add-mask::fixture-secret\n{"type":"assistant.message","data":{"content":"fixture-secret"}}\n';
const result = parseDeepSeekLog(raw);

expect(result.logEntries).toEqual(events);
expect(normalizeAgentSession(result.logEntries)).toEqual(events);
expect(parseDeepSeekLog(serializeSessionArtifact(result.logEntries)).logEntries).toEqual(events);
expect(isDeepSeekLog(raw)).toBe(false);
expect(projectSessionResult(result.logEntries)).toMatchObject({ num_turns: 0, duration_ms: 0, total_cost_usd: 0, usage: { input_tokens: 2, output_tokens: 3, total_tokens: 5 } });
expect(projectSessionResult(result.logEntries).usage.total_tokens).toBe(5);
});

it("retains a single canonical record and valid records adjacent to a malformed partial line", () => {
expect(parseDeepSeekLog(JSON.stringify(events[10], null, 2)).logEntries).toEqual([events[10]]);
const result = parseDeepSeekLog(`${JSON.stringify(events[2])}\n{"partial":\n${JSON.stringify(events[4])}\n`);

expect(result.logEntries).toEqual([events[2], events[4]]);
expect(result.markdown).toContain("coverage is partial");
expect(result.logEntries).toEqual([]);
expect(JSON.stringify(result)).not.toContain("fixture-secret");
expect(result.markdown).toContain("Raw content is omitted");
});

it.each([fixture, startupFailure, timedOut, events.map(JSON.stringify).join("\n")])("persists the engine-owned trace through canonical and unified artifacts", raw => {
it.each([fixture, startupFailure, timedOut])("persists the observed headless trace through canonical and unified artifacts", raw => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepseek-session-audit-"));
try {
const parsed = parseDeepSeekLog(raw).logEntries;
fs.writeFileSync(path.join(root, "agent-stdio.log"), raw);
writeSessionArtifact(path.join(root, "agent-session.jsonl"), parsed);
expect(fs.readFileSync(path.join(root, "agent-session.jsonl"), "utf8")).toBe(serializeSessionArtifact(parsed));
expect(JSON.parse(fs.readFileSync(path.join(root, "agent-session.jsonl"), "utf8").split("\n")[0]).data.provider).toBe("github");
const warnings = [];
const canonical = collectUnifiedSession({ rootDir: root, engine: "deepseek-harness", dailyAIC: {}, warn: warning => warnings.push(warning) }).events;
fs.unlinkSync(path.join(root, "agent-session.jsonl"));
Expand All @@ -270,16 +273,8 @@ describe("DeepSeek direct canonical session inputs", () => {
expect(event.provenance.path).toBe("agent-stdio.log");
expect(event.provenance.phase).toBe("agent");
}
if (parsed.some(event => event.type === "vendor.unknown")) {
const { metadata, ...essentialExtension } = events[10];
expect(persisted.find(event => event.type === "vendor.unknown")).toMatchObject(essentialExtension);
expect(persisted.find(event => event.type === "tool.execution_start" && event.data.toolCallId === "call-1").data.input).toEqual(events[4].data.input);
expect(persisted.find(event => event.type === "assistant.refusal").data).toEqual(events[6].data);
expect(persisted.filter(event => event.type === "assistant.reasoning").map(event => event.data.content)).toEqual(["hello", " \n"]);
expect(persisted.filter(event => event.type === "session.result").map(event => event.data.usage)).toEqual([{ inputTokens: 2, outputTokens: 3 }, { totalTokens: 5 }]);
} else {
expect(persisted.some(event => event.type === "session.result" || event.type === "assistant.refusal" || event.type.startsWith("tool.") || event.type === "assistant.reasoning")).toBe(false);
}
expect(persisted.find(event => event.type === "session.init").data).toEqual({ sourceEngine: "deepseek-harness", model: parsed[0].data.model });
expect(persisted.some(event => event.type === "session.result" || event.type === "assistant.refusal" || event.type.startsWith("tool.") || event.type === "assistant.reasoning")).toBe(false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
Expand Down
11 changes: 7 additions & 4 deletions docs/src/content/docs/reference/engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,19 @@ GitHub toolsets, cache memory, and custom MCP servers remain configured on that
transport. This does not enable restrictions on dsh's native shell/file tools;
restricted bash command allowlists are rejected at compilation.

DeepSeek headless session parsing retains the configured provider/model even
when execution fails or stdout is truncated. Completed, unambiguous stdout
DeepSeek headless session parsing retains the configured provider/model in
`logEntries` and `agent-session.jsonl` even when execution fails or stdout is
truncated. The essential `aw_session.jsonl` projection retains engine/model but
omits duplicated provider metadata. Completed, unambiguous stdout
becomes `assistant.message`; attributed Node startup errors and harness failures
become `session.error`, with observed failure exits in `agent.execution`.
Unattributed partial output and wrapper diagnostics are omitted. This profile
does not expose native reasoning, tool arguments/results, refusals, or session
accounting, so those observations are not inferred from answer text or firewall
usage. A configured model such as `auto` is not a resolved model observation,
and process exit zero does not establish task success. Canonical session inputs
are preserved, but this is not a native dsh JSON transcript format.
and process exit zero does not establish task success. Shared session readers
handle canonical artifacts; the stdout parser does not accept an additional
native dsh JSON transcript format.

The OpenCode sample routes `copilot`, `anthropic`, and `openai`/`codex` models
through the selected AWF endpoint, retains configured MCP tools, and uses native
Expand Down
Loading