diff --git a/internal/backend/host.go b/internal/backend/host.go index 3c84d53..3ab2993 100644 --- a/internal/backend/host.go +++ b/internal/backend/host.go @@ -691,6 +691,20 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error { return nil }), ), + // The always-local extension probes NetworkService/IsConnected roughly 10s + // after any slow request starts. A 404 here is treated as "network + // disconnected" and aborts in-flight work (e.g. commit message + // generation) even while the model is still streaming. Always answer OK. + server.POST("/aiserver.v1.NetworkService/IsConnected", + server.Name("network_is_connected"), + server.ConnectUnary(), + server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{ + Name: "network_is_connected", + StatusCode: http.StatusOK, + MockProtoType: "aiserver.v1.IsConnectedResponse", + MockBuilder: upstream.EmptyMockBuilder, + })), + ), server.Any("/aiserver.v1.NetworkService/*", server.Name("network_service"), server.HTTP(), diff --git a/internal/backend/server/upstream/client.go b/internal/backend/server/upstream/client.go index dfedcd8..bc23bb7 100644 --- a/internal/backend/server/upstream/client.go +++ b/internal/backend/server/upstream/client.go @@ -451,6 +451,8 @@ func newProtoMessage(typeName string) (proto.Message, error) { return &aiserverv1.SubmitLogsResponse{}, nil case "aiserver.v1.TrackEventsResponse": return &aiserverv1.TrackEventsResponse{}, nil + case "aiserver.v1.IsConnectedResponse": + return &aiserverv1.IsConnectedResponse{}, nil default: return nil, fmt.Errorf("unsupported proto message type %q", typeName) } diff --git a/internal/cursor/state_db.go b/internal/cursor/state_db.go index 940c0f1..e5ca304 100644 --- a/internal/cursor/state_db.go +++ b/internal/cursor/state_db.go @@ -33,6 +33,14 @@ var cursorStateDisabledStatsigGates = []string{ "disable_terminal_output_ui_streaming", } +// cursorStateEnabledStatsigGates forces gates to true. Disabling the network +// change monitor stops the always-local extension from probing +// NetworkService/IsConnected and aborting slow in-flight requests (such as +// commit message generation) with "Network disconnected". +var cursorStateEnabledStatsigGates = []string{ + "disable_network_change_monitor_local", +} + // InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the // Settings page. It does not modify the installed Cursor app bundle. func InjectCursorUserInfo(email, token string) error { @@ -50,12 +58,13 @@ func InjectCursorUserInfo(email, token string) error { } logger.Infof( - "injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s", + "injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s enabled_statsig_gates=%s", stateDBPath, values["cursorAuth/cachedEmail"], values["cursorAuth/stripeMembershipType"], values["cursorAuth/stripeSubscriptionStatus"], strings.Join(cursorStateDisabledStatsigGates, ","), + strings.Join(cursorStateEnabledStatsigGates, ","), ) return nil } @@ -120,7 +129,7 @@ func syncCursorAuthStateDB(path string, values map[string]string) error { } } - if err := disableCursorStatsigGates(ctx, tx); err != nil { + if err := syncCursorStatsigGateOverrides(ctx, tx); err != nil { return err } @@ -131,7 +140,7 @@ func syncCursorAuthStateDB(path string, values map[string]string) error { return nil } -func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error { +func syncCursorStatsigGateOverrides(ctx context.Context, tx *sql.Tx) error { var raw []byte err := tx.QueryRowContext(ctx, "SELECT value FROM ItemTable WHERE key = ?", cursorStateStatsigBootstrapKey).Scan(&raw) if err != nil { @@ -154,9 +163,15 @@ func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error { hashUsed, _ := payload["hash_used"].(string) for _, gate := range cursorStateDisabledStatsigGates { - disableCursorStatsigGate(featureGates, gate) + setCursorStatsigGate(featureGates, gate, false, "local_disabled") if strings.EqualFold(hashUsed, "djb2") { - disableCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate)) + setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), false, "local_disabled") + } + } + for _, gate := range cursorStateEnabledStatsigGates { + setCursorStatsigGate(featureGates, gate, true, "local_enabled") + if strings.EqualFold(hashUsed, "djb2") { + setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), true, "local_enabled") } } @@ -170,21 +185,21 @@ func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error { return nil } -func disableCursorStatsigGate(featureGates map[string]any, key string) { +func setCursorStatsigGate(featureGates map[string]any, key string, value bool, rule string) { gate, _ := featureGates[key].(map[string]any) if gate == nil { gate = map[string]any{ "name": key, - "rule_id": "local_disabled", - "ruleID": "local_disabled", - "group_name": "local_disabled", - "groupName": "local_disabled", + "rule_id": rule, + "ruleID": rule, + "group_name": rule, + "groupName": rule, "id_type": "userID", "idType": "userID", } featureGates[key] = gate } - gate["value"] = false + gate["value"] = value } func cursorStateDJB2Hash(value string) string {