From ce519dbc0c46e283fbde1ffce7e5b0d3123dd513 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 30 May 2026 04:09:09 +0000 Subject: [PATCH] fix: align apply-patch failure flow with codex --- docs/reference/cli.md | 2 +- scripts/src/apply-patch-v2.ts | 13 +------------ scripts/src/help.ts | 2 +- scripts/ssh-argv-guidance-contract-test.ts | 5 ++--- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index c217e745..9c84d30a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -29,7 +29,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P - `server rebuild ` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`;该命令用于替代手工删除容器的兜底流程,其中 `dev-frontend-proxy` 只更新主 server dev 入口薄代理,`todo-note`、`code-queue-mgr`、`project-manager`、`baidu-netdisk` 和 `oa-event-flow` 只重建主 server 承载的对应后端,不会重建或删除 database 命名卷。D601 Code Queue 执行面不由 `server rebuild` 管理,Rust backend-core 迭代不得用 `server rebuild backend-core` 在 master server 编译,规则见 `docs/reference/dev-environment.md`。 - `provider attach [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,`provider-.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build`。`provider triage [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]` 是只读多信号健康裁决入口,会把单路径 `provider is not online`、SSH 超时、registry 失败和 service proxy 失败归类成 `runner-local-observation-gap`、`service-degraded`、`provider-degraded` 或 `global-blocker`。默认输出只返回裁决、scope、失败/降级/未知信号和有界 evidence 摘要,完整 evidence 必须显式加 `--full` 或 `--raw`;推荐交叉验证命令仍包含 `debug health`、`debug dispatch host.ssh --wait-ms 15000`、`ssh argv true`、`artifact-registry health --provider-id `、`microservice health k3sctl-adapter`、`microservice health code-queue` 和 `codex tasks --view supervisor --limit 20`。 - `ssh [operation args...]` / `tran [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601` 或 `G14`,也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `D601:win`、`D601:win/c/test`、`G14:k3s`、`D601:k3s` 或 `G14:k3s::`。WSL provider 的 Windows cmd 入口固定写 `tran D601:win cmd `,需要 Windows cwd 时用 `tran D601:win/c/test cmd cd`,由 CLI 自动设置 `chcp 65001`、`PYTHONUTF8=1` 和 `PYTHONIOENCODING=utf-8`;命名只允许 `win`,不得使用 `win32`。非交互远端命令优先使用 `ssh argv ...`;需要 shell 脚本、管道、变量或循环时优先使用 quoted heredoc 单步传输,例如 `tran G14 script <<'SCRIPT'`、`tran G14:k3s script <<'SCRIPT'` 或 `tran G14:k3s:: script <<'SCRIPT'`,把脚本走 stdin。`script -- '<单个字符串>'` 是无需 stdin 的远端 shell one-liner,例如 `tran G14:/root/hwlab script -- 'cd /root/hwlab && git status --short --branch'`;`script -- <多个 argv>` 才是 direct argv,适合 `tran D601:/path script -- sed -n '1,20p' file` 这类带短横线的单进程命令。顶层 remote option parser 必须保留命令已经开始后的 `--`,不得把它吞成全局选项结束符。需要远端改文本文件时默认优先使用 ` apply-patch < patch.diff`;需要可靠传输非文本或整文件时使用 ` upload ` 和 ` download `,CLI 会按字节数与 SHA-256 自动校验并在 provider-gateway stdin/argv 限制下切换客户端分块策略;需要旧 helper 时显式使用 `:k3s:: apply-patch-v1` 或 ` apply-patch-v1`。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示 stdin script/argv 重试和 provider triage 交叉验证。 -- `ssh apply-patch < patch.diff` 是默认推荐的远端 patch 入口:本地 TypeScript line-based engine 解析和计算新文件内容,远端 route 只负责读写文件;支持 host workspace、k3s pod workspace、Windows workspace route(例如 `D601:win/c/test`)和 frontend transport,并优先处理长中文/Unicode、低上下文插入、重复块 `@@` 定位等旧 helper 容易失败的场景。`apply-patch` 输出按 Codex 标准文本口径,不套 UniDesk JSON 限制:成功 stdout 为 `Success. Updated the following files:`,失败 stdout 为空、stderr 写失败原因;多文件补丁中途失败时,stderr 还会直接列出 `Applied before failure`、`Failed` 和 `Pending after failure`,避免再二次查询哪些文件已写。Windows route 复用同一套 v2 核心算法,只把底层读写替换成 PowerShell 文件系统接口;`ssh apply-patch-v1 [tool args...] < patch.diff` 保留为 v1 fallback,直接调用远端注入的 `apply_patch` sh/perl helper;只有默认 v2 引擎出现问题、需要复用旧 helper 行为或人工确认 `--allow-loose` 时才优先使用 v1。 +- `ssh apply-patch < patch.diff` 是默认推荐的远端 patch 入口:本地 TypeScript line-based engine 解析和计算新文件内容,远端 route 只负责读写文件;支持 host workspace、k3s pod workspace、Windows workspace route(例如 `D601:win/c/test`)和 frontend transport,并优先处理长中文/Unicode、低上下文插入、重复块 `@@` 定位等旧 helper 容易失败的场景。`apply-patch` 输出按 Codex 标准文本口径,不套 UniDesk JSON 限制:成功 stdout 为 `Success. Updated the following files:`,失败 stdout 为空、stderr 写失败原因;多文件补丁中途失败时,stderr 只列出第一个失败前已成功执行的 hunk 和失败 hunk,随后按 Codex 语义停止,不继续尝试后续 hunk。Windows route 复用同一套 v2 核心算法,只把底层读写替换成 PowerShell 文件系统接口;`ssh apply-patch-v1 [tool args...] < patch.diff` 保留为 v1 fallback,直接调用远端注入的 `apply_patch` sh/perl helper;只有默认 v2 引擎出现问题、需要复用旧 helper 行为或人工确认 `--allow-loose` 时才优先使用 v1。 - `ssh py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。 - `ssh skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills` 与 `.codex/skills`。 - `ssh :k3s[:namespace:workload[:container]] ...` 是原生 k3s 结构化 route 入口,route 只定位控制面或 workload,`kubectl`、`logs`、`exec`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback 和普通容器命令作为 operation 放在 route 之后;CLI 固定注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并把 kubectl、workload exec、logs 和 pod workspace 读写参数组装成 argv,避免在 Host SSH、bash、kubectl exec 和容器 shell 之间反复手写多层引号;D601 与 G14 都有 provider-specific guard,分别校验 `d601` 和 G14 k3s 节点身份。 diff --git a/scripts/src/apply-patch-v2.ts b/scripts/src/apply-patch-v2.ts index ab521a9d..f3bb7f08 100644 --- a/scripts/src/apply-patch-v2.ts +++ b/scripts/src/apply-patch-v2.ts @@ -26,11 +26,10 @@ export interface PatchUpdateResult { export interface ApplyPatchV2Outcome { hunk: number; action: PatchHunk["kind"] | "move"; - status: "applied" | "failed" | "pending"; + status: "applied" | "failed"; path: string; targetPath?: string; change?: string; - reason?: string; partialChanges?: string[]; error?: { name?: string; @@ -296,18 +295,10 @@ async function applyPatchV2Hunks(executor: ApplyPatchV2Executor, hunks: PatchHun ...(partialChanges.length > 0 ? { partialChanges } : {}), error: errorSummary(error), }); - for (let pendingIndex = index + 1; pendingIndex < hunks.length; pendingIndex += 1) { - outcomes.push({ - ...outcomeBase(hunks[pendingIndex] as PatchHunk, pendingIndex), - status: "pending", - reason: "skipped_after_previous_failure", - }); - } throw new ApplyPatchV2Error(error instanceof Error ? error.message : String(error), { partialChanges: changed, outcomes, failed: outcomes.find((item) => item.status === "failed") ?? null, - pending: outcomes.filter((item) => item.status === "pending"), cause: error instanceof ApplyPatchV2Error ? error.details : undefined, }); } @@ -342,7 +333,6 @@ function formatApplyPatchFailure(error: unknown): string { lines.push("Patch status:"); appendOutcomeSection(lines, "Applied before failure:", outcomes.filter((item) => item.status === "applied")); appendOutcomeSection(lines, "Failed:", outcomes.filter((item) => item.status === "failed")); - appendOutcomeSection(lines, "Pending after failure:", outcomes.filter((item) => item.status === "pending")); } return `${lines.join("\n")}\n`; } @@ -359,7 +349,6 @@ function formatOutcome(outcome: ApplyPatchV2Outcome): string { const target = outcome.targetPath === undefined ? outcome.path : `${outcome.path} -> ${outcome.targetPath}`; const base = `hunk ${outcome.hunk} ${outcome.action} ${target}`; if (outcome.status === "applied") return outcome.change === undefined ? base : `${outcome.change} (${base})`; - if (outcome.status === "pending") return `${base} (pending)`; const error = outcome.error?.message ?? "failed"; const partial = outcome.partialChanges === undefined || outcome.partialChanges.length === 0 ? "" diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 834acb1f..98bd58f3 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -191,7 +191,7 @@ export function sshHelp(): unknown { "When a one-line shell command is easier to type through the script path, `script -- ''` runs that single string through the remote shell without waiting for stdin. When `script --` is followed by multiple tokens, it stays a direct argv form for commands such as `tran D601:/work script -- sed -n '1,20p' file`.", "script and shell helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.", "For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.", - "`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Its stdout/stderr follows Codex apply_patch text output rather than UniDesk JSON output; on multi-file failure, stderr lists applied, failed and pending hunks.", + "`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Its stdout/stderr follows Codex apply_patch text output rather than UniDesk JSON output; on multi-file failure, stderr lists applied hunks before the first failed hunk and the failed hunk, then stops like Codex apply_patch.", "`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and fall back from a single stdin payload to bounded client-side chunks before treating provider-gateway limits as a server-side problem.", "`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.", "script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.", diff --git a/scripts/ssh-argv-guidance-contract-test.ts b/scripts/ssh-argv-guidance-contract-test.ts index 10ae20e1..a05ebd47 100644 --- a/scripts/ssh-argv-guidance-contract-test.ts +++ b/scripts/ssh-argv-guidance-contract-test.ts @@ -772,9 +772,8 @@ export async function runSshArgvGuidanceContract(): Promise { && failedCompoundVisibleV2.stderr.includes("M first.txt") && failedCompoundVisibleV2.stderr.includes("Failed:") && failedCompoundVisibleV2.stderr.includes("hunk 2 update second.txt") - && failedCompoundVisibleV2.stderr.includes("Pending after failure:") - && failedCompoundVisibleV2.stderr.includes("hunk 3 update third.txt"), - "v2 failed CLI path should print Codex-style stderr plus per-file applied/failed/pending summary", + && !failedCompoundVisibleV2.stderr.includes("third.txt"), + "v2 failed CLI path should print Codex-style stderr plus applied/failed summary and stop before later hunks", failedCompoundVisibleV2.stderr, );