From 2779a2dc84139437d2713f2817c03c54a83613ee Mon Sep 17 00:00:00 2001 From: Alex Godoroja Date: Tue, 9 Jun 2026 15:44:33 -0700 Subject: [PATCH] fix(pilotctl): make appstore call timeout configurable (default 120s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appstore call hardcoded an 8s socket read deadline, which cut off any legitimately slow method (multi-step research, cold LLM synthesis) long before it could return — surfacing as a spurious 'i/o timeout' even though the app and backend were healthy. Default to 120s and allow per-call --timeout / $PILOT_APPSTORE_CALL_TIMEOUT. The 8s was a reply deadline, not a dial timeout; dialing the local socket stays fast. --- cmd/pilotctl/appstore.go | 43 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/cmd/pilotctl/appstore.go b/cmd/pilotctl/appstore.go index 5f02f40e..7ebf8fe8 100644 --- a/cmd/pilotctl/appstore.go +++ b/cmd/pilotctl/appstore.go @@ -114,8 +114,10 @@ Usage: pilotctl appstore caps show the manifest's spend caps and current rolling-window usage pilotctl appstore actions [--tail N] [--event NAME] show the pilotctl-side action log (install/uninstall — survives app removal) - pilotctl appstore call [json-args] + pilotctl appstore call [json-args] [--timeout 2m] dispatch an IPC call into an app + (--timeout / $PILOT_APPSTORE_CALL_TIMEOUT; + default 120s — raise for slow methods) Install root is taken from $PILOT_APPSTORE_ROOT or ~/.pilot/apps. ` @@ -1894,10 +1896,41 @@ func cmdAppStoreActions(args []string) { // ── call ─────────────────────────────────────────────────────────────── +// callTimeoutDefault bounds how long `appstore call` waits for an app's reply. +// It must comfortably exceed the slowest legitimate method (e.g. multi-step +// research / cold LLM synthesis), which can run tens of seconds. Override per +// call with --timeout, or globally with $PILOT_APPSTORE_CALL_TIMEOUT. +const callTimeoutDefault = 120 * time.Second + func cmdAppStoreCall(args []string) { + // Resolve the reply timeout (env default, then --timeout flag) and strip + // the flag from the positional args so [json] still parse. + callTimeout := callTimeoutDefault + if v := os.Getenv("PILOT_APPSTORE_CALL_TIMEOUT"); v != "" { + if d, err := time.ParseDuration(v); err == nil && d > 0 { + callTimeout = d + } + } + var pos []string + for i := 0; i < len(args); i++ { + switch { + case args[i] == "--timeout" && i+1 < len(args): + if d, err := time.ParseDuration(args[i+1]); err == nil && d > 0 { + callTimeout = d + } + i++ + case strings.HasPrefix(args[i], "--timeout="): + if d, err := time.ParseDuration(strings.TrimPrefix(args[i], "--timeout=")); err == nil && d > 0 { + callTimeout = d + } + default: + pos = append(pos, args[i]) + } + } + args = pos if len(args) < 2 { fatalHint("invalid_argument", - "usage: pilotctl appstore call [json-args]", + "usage: pilotctl appstore call [json-args] [--timeout 2m]", "missing arguments") } appID := args[0] @@ -1920,7 +1953,7 @@ func cmdAppStoreCall(args []string) { "socket %s not present: %v", sockPath, err) } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() dialer := &net.Dialer{Timeout: 3 * time.Second} conn, err := dialer.DialContext(ctx, "unix", sockPath) @@ -1930,7 +1963,9 @@ func cmdAppStoreCall(args []string) { "dial %s: %v", sockPath, err) } defer conn.Close() - _ = conn.SetDeadline(time.Now().Add(8 * time.Second)) + // Reply deadline (not the dial): bounds the whole call so a slow method + // (research, cold LLM) isn't cut off. Tune with --timeout / env. + _ = conn.SetDeadline(time.Now().Add(callTimeout)) // args is a JSON value, but ipc.Call takes a Go value. Marshal it // back into a json.RawMessage so the wrapper round-trips cleanly.