mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* fix(agent): deliver the copilot prompt on stdin; name E2BIG start failures (MUL-8105) Copilot inlined the whole task prompt as -p <prompt>, so a thread above Linux's 131,072-byte MAX_ARG_STRLEN died at fork/exec with E2BIG (#9362). The prompt now rides stdin, mirroring the OpenCode move in #6552. startOwnedProcessTree also wraps oversized-command-line failures in ErrArgvLimitExceeded with an actionable remedy. Co-authored-by: multica-agent <[email protected]> * docs(agent): update copilotBackend comment for stdin prompt delivery (MUL-8105) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
107 lines
4.8 KiB
Go
107 lines
4.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// chooseCopilotInvocation selects the actual program (argv[0]) and the full
|
|
// argv to spawn a Copilot CLI run.
|
|
//
|
|
// On macOS/Linux the npm binstub is a symlink to npm-loader.js with a
|
|
// `#!/usr/bin/env node` shebang, so execve hands argv to node unchanged and
|
|
// node's spawnSync forwards it to the bundled native binary unchanged.
|
|
//
|
|
// On Windows there is no shebang, so npm ships copilot.cmd/copilot.ps1
|
|
// launchers and every layer in between re-serialises argv onto a new command
|
|
// line. Both launchers are lossy for the multi-line, quote-bearing -p prompt
|
|
// built by buildCopilotArgs; the symptom is Copilot's own
|
|
// "It looks like your prompt was not quoted, so the extra words were treated
|
|
// as separate arguments" error. The Windows rewrite in
|
|
// copilot_invocation_windows.go therefore skips the launchers and spawns the
|
|
// bundled copilot.exe directly.
|
|
func chooseCopilotInvocation(execName, lookedUp string, args []string, logger *slog.Logger) (string, []string) {
|
|
if argv0, full, ok := platformCopilotInvocation(lookedUp, args, logger); ok {
|
|
return argv0, full
|
|
}
|
|
return execName, args
|
|
}
|
|
|
|
// resolveCopilotNativeFromShim returns the path to the native Copilot
|
|
// executable bundled inside the npm package, given the path to the npm
|
|
// `copilot.cmd` shim that PATH lookup found on Windows. Returns "" if the
|
|
// shim doesn't end in `.cmd` or no candidate platform package ships a binary
|
|
// at either known location, in which case the caller falls back to the
|
|
// PowerShell launcher.
|
|
//
|
|
// Why bypass the launchers instead of quoting harder: nothing on the Go side
|
|
// can make them lossless. Both hops re-parse a command line with rules that
|
|
// differ from the CRT rules Go's os/exec escapes for.
|
|
//
|
|
// - copilot.cmd forwards with `%*`, which cmd.exe expands by re-tokenising
|
|
// the raw command line — newlines and quotes in the -p prompt do not
|
|
// survive.
|
|
// - copilot.ps1 ends in `& node.exe npm-loader.js $args`, and PowerShell
|
|
// re-serialises $args onto node's command line. Under Windows PowerShell
|
|
// 5.1 (and pwsh <= 7.2, which default to Legacy native argument passing)
|
|
// embedded double quotes are not re-escaped, so a prompt containing them
|
|
// is re-tokenised — the same defect already documented for cursor-agent
|
|
// in buildCursorArgs (#5649).
|
|
//
|
|
// The task prompt itself now rides stdin (see buildCopilotArgs, #9362), but
|
|
// model ids, session ids and custom_args still travel on argv, so the native
|
|
// binary stays preferred. Spawning copilot.exe directly leaves exactly one
|
|
// hop, Go -> native binary, and Go's syscall.EscapeArg is the exact inverse of
|
|
// the CRT parsing the binary uses.
|
|
//
|
|
// Layout when installed via `npm install -g @github/copilot` (verified
|
|
// against @github/copilot 1.0.77):
|
|
//
|
|
// <prefix>\copilot.cmd (shim)
|
|
// <prefix>\node_modules\@github\copilot\npm-loader.js (JS entry)
|
|
// <prefix>\node_modules\@github\copilot\node_modules\@github\copilot-win32-x64\copilot.exe (native)
|
|
//
|
|
// npm keeps the platform package nested under the parent package; older npm
|
|
// versions and other package managers hoist it to the top-level
|
|
// node_modules instead, so both locations are probed.
|
|
//
|
|
// statFn is injected so this is testable on non-Windows hosts.
|
|
func resolveCopilotNativeFromShim(shimPath string, statFn func(string) (os.FileInfo, error)) string {
|
|
if !strings.EqualFold(filepath.Ext(shimPath), ".cmd") {
|
|
return ""
|
|
}
|
|
prefix := filepath.Dir(shimPath)
|
|
for _, pkg := range copilotWindowsPackageCandidates(runtime.GOARCH) {
|
|
candidates := []string{
|
|
// npm's current layout: optional platform dep nested under the parent.
|
|
filepath.Join(prefix, "node_modules", "@github", "copilot", "node_modules", "@github", pkg, "copilot.exe"),
|
|
// Hoisted layout used by older npm versions and other installers.
|
|
filepath.Join(prefix, "node_modules", "@github", pkg, "copilot.exe"),
|
|
}
|
|
for _, candidate := range candidates {
|
|
if _, err := statFn(candidate); err == nil {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// copilotWindowsPackageCandidates returns the npm platform package names that
|
|
// may host the bundled copilot.exe, ordered so the most likely match for the
|
|
// given GOARCH comes first. ARM64 hosts try the arm64 build first; everything
|
|
// else tries x64 first, because an x64 Node running under emulation on an
|
|
// ARM64 host installs the x64 platform package. Cost is one extra statFn call
|
|
// per miss when the GOARCH-preferred package isn't installed.
|
|
func copilotWindowsPackageCandidates(goarch string) []string {
|
|
switch goarch {
|
|
case "arm64":
|
|
return []string{"copilot-win32-arm64", "copilot-win32-x64"}
|
|
default:
|
|
return []string{"copilot-win32-x64", "copilot-win32-arm64"}
|
|
}
|
|
}
|